perf: recall BFS N+1 优化 + timeRange 过滤支持

- GraphDatabase.recall() 添加 timeRange.days 参数,计算 minDateBucket 过滤
- getRelationsForNodes() 批量预加载节点名称缓存,减少 N+1 查询
- 关系查询传入 minDateBucket,支持按时间范围过滤关系
- BFS 扩展节点时优先从缓存获取,避免重复查询数据库
This commit is contained in:
root
2026-04-30 15:09:53 +08:00
parent ba44d50d7a
commit dd0ab4dcbe

View File

@ -249,10 +249,17 @@ export class GraphDatabase {
return await this.store.insert('nodes', bucket);
}
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, sessionFilter?: string): Promise<RecallResult> {
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, timeRange?: TimeRangeParams, sessionFilter?: string): Promise<RecallResult> {
if (!this.store) {
return { entities: [], relations: [], message: 'Database not initialized' };
}
// 计算时间范围过滤
let minDateBucket: string | undefined;
if (timeRange && timeRange.days && timeRange.days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - timeRange.days);
minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
}
const keywords = queryIntent.toLowerCase().replace(/,/g, ' ').split(/\s+/).filter(w => w.trim());
const allEntities: BfsEntity[] = [];
const entityIds = new Set<number>();
@ -318,10 +325,14 @@ export class GraphDatabase {
const allRelations: RelationQueryResult[] = [];
let currentLayerIds = new Set<number>(entityIds);
const visitedEntityIds = new Set<number>(entityIds);
// 批量预加载所有相关节点名称,减少 N+1 查询
const nodeNameCache = new Map<number, {name: string, type: string, mentions: number}>();
// 批量预加载所有相关节点名称,减少 N+1 查询
const nodeNameCache = new Map<number, {name: string, type: string, mentions: number}>();
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
const currentIds = Array.from(currentLayerIds);
const relations = await this.getRelationsForNodes(currentIds, sessionFilter);
const relations = await this.getRelationsForNodes(currentIds, sessionFilter, minDateBucket);
const nextLayerIds = new Set<number>();
for (const rel of relations) {
@ -337,16 +348,30 @@ export class GraphDatabase {
for (const newId of nextLayerIds) {
if (!visitedEntityIds.has(newId)) {
visitedEntityIds.add(newId);
const nodeData = await this.getNodeById(newId);
if (nodeData) {
// 优先从缓存获取,避免 N+1 查询
const cached = nodeNameCache.get(newId);
if (cached) {
const addedEntity: BfsEntity = {
id: nodeData.id,
name: nodeData.name,
type: nodeData.type,
mentions: nodeData.mentions,
id: newId,
name: cached.name,
type: cached.type,
mentions: cached.mentions,
depth: layer + 1
};
allEntities.push(addedEntity);
} else {
const nodeData = await this.getNodeById(newId);
if (nodeData) {
nodeNameCache.set(newId, {name: nodeData.name, type: nodeData.type, mentions: nodeData.mentions});
const addedEntity: BfsEntity = {
id: nodeData.id,
name: nodeData.name,
type: nodeData.type,
mentions: nodeData.mentions,
depth: layer + 1
};
allEntities.push(addedEntity);
}
}
}
}
@ -402,21 +427,32 @@ export class GraphDatabase {
return null;
}
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string): Promise<RelationQueryResult[]> {
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string, minDateBucket?: string): Promise<RelationQueryResult[]> {
if (!this.store || nodeIds.length === 0) return [];
const relations: RelationQueryResult[] = [];
// 批量预加载所有节点名称到缓存,避免 N+1 查询
const nodeNameCache = new Map<number, {name: string, type: string, mentions: number}>();
for (const id of nodeIds) {
const node = await this.getNodeById(id);
if (node) {
nodeNameCache.set(id, {name: node.name, type: node.type, mentions: node.mentions});
}
}
for (const nodeId of nodeIds) {
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'active').and().equalTo('subject_id', nodeId);
if (sessionFilter) {
predicates.and().equalTo('session_id', sessionFilter);
}
if (minDateBucket) {
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
const sourceNode = await this.getNodeById(sourceId);
const targetNode = await this.getNodeById(targetId);
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
if (sourceNode && targetNode) {
relations.push({
sourceId,
@ -438,12 +474,15 @@ export class GraphDatabase {
if (sessionFilter) {
predicates2.and().equalTo('session_id', sessionFilter);
}
if (minDateBucket) {
predicates2.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet2: relationalStore.ResultSet = await this.store.query(predicates2, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
while (resultSet2.goToNextRow()) {
const sourceId = resultSet2.getLong(resultSet2.getColumnIndex('subject_id'));
const targetId = resultSet2.getLong(resultSet2.getColumnIndex('object_id'));
const sourceNode = await this.getNodeById(sourceId);
const targetNode = await this.getNodeById(targetId);
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
if (sourceNode && targetNode) {
relations.push({
sourceId,