mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +08:00
fix(server): frequent embedding (#13475)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Smarter embedding pipeline skips re-embedding when content hasn’t changed; added content sanitization for embeddings and workspace content retrieval. - Bug Fixes - Re-embedding now requires both a document update and the last embedding being older than 10 minutes, reducing unnecessary work. - Refactor - Consolidated embedding preprocessing and moved sanitization utilities into shared models; upserts now refresh stored content. - Tests - Expanded snapshot-based tests covering multiple time/age scenarios for embedding decision logic. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -148,3 +148,36 @@ export type IgnoredDoc = {
|
||||
createdByAvatar: string | undefined;
|
||||
updatedBy: string | undefined;
|
||||
};
|
||||
|
||||
export const EMBEDDING_DIMENSIONS = 1024;
|
||||
|
||||
const FILTER_PREFIX = [
|
||||
'Title: ',
|
||||
'Created at: ',
|
||||
'Updated at: ',
|
||||
'Created by: ',
|
||||
'Updated by: ',
|
||||
];
|
||||
|
||||
export function clearEmbeddingContent(content: string): string {
|
||||
const lines = content.split('\n');
|
||||
let maxLines = 5;
|
||||
while (maxLines > 0 && lines.length > 0) {
|
||||
if (FILTER_PREFIX.some(prefix => lines[0].startsWith(prefix))) {
|
||||
lines.shift();
|
||||
maxLines--;
|
||||
} else {
|
||||
// only process consecutive metadata rows
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function clearEmbeddingChunk(chunk: ChunkSimilarity): ChunkSimilarity {
|
||||
if (chunk.content) {
|
||||
const content = clearEmbeddingContent(chunk.content);
|
||||
return { ...chunk, content };
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Prisma } from '@prisma/client';
|
||||
import { CopilotSessionNotFound } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import {
|
||||
clearEmbeddingContent,
|
||||
ContextBlob,
|
||||
ContextConfigSchema,
|
||||
ContextDoc,
|
||||
@@ -13,14 +14,13 @@ import {
|
||||
CopilotContext,
|
||||
DocChunkSimilarity,
|
||||
Embedding,
|
||||
EMBEDDING_DIMENSIONS,
|
||||
FileChunkSimilarity,
|
||||
MinimalContextConfigSchema,
|
||||
} from './common/copilot';
|
||||
|
||||
type UpdateCopilotContextInput = Pick<CopilotContext, 'config'>;
|
||||
|
||||
export const EMBEDDING_DIMENSIONS = 1024;
|
||||
|
||||
/**
|
||||
* Copilot Job Model
|
||||
*/
|
||||
@@ -215,8 +215,9 @@ export class CopilotContextModel extends BaseModel {
|
||||
select: { content: true },
|
||||
orderBy: { chunk: 'asc' },
|
||||
});
|
||||
return file?.map(f => f.content).join('\n');
|
||||
return file?.map(f => clearEmbeddingContent(f.content)).join('\n');
|
||||
}
|
||||
|
||||
async insertFileEmbedding(
|
||||
contextId: string,
|
||||
fileId: string,
|
||||
@@ -263,6 +264,19 @@ export class CopilotContextModel extends BaseModel {
|
||||
return similarityChunks.filter(c => Number(c.distance) <= threshold);
|
||||
}
|
||||
|
||||
async getWorkspaceContent(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
chunk?: number
|
||||
): Promise<string | undefined> {
|
||||
const file = await this.db.aiWorkspaceEmbedding.findMany({
|
||||
where: { workspaceId, docId, chunk },
|
||||
select: { content: true },
|
||||
orderBy: { chunk: 'asc' },
|
||||
});
|
||||
return file?.map(f => clearEmbeddingContent(f.content)).join('\n');
|
||||
}
|
||||
|
||||
async insertWorkspaceEmbedding(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
@@ -287,6 +301,7 @@ export class CopilotContextModel extends BaseModel {
|
||||
VALUES ${values}
|
||||
ON CONFLICT (workspace_id, doc_id, chunk)
|
||||
DO UPDATE SET
|
||||
content = EXCLUDED.content,
|
||||
embedding = EXCLUDED.embedding,
|
||||
updated_at = excluded.updated_at;
|
||||
`;
|
||||
|
||||
@@ -242,10 +242,9 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
@Transactional()
|
||||
async checkDocNeedEmbedded(workspaceId: string, docId: string) {
|
||||
// NOTE: check if the document needs re-embedding.
|
||||
// 1. check if there have been any recent updates to the document snapshot and update
|
||||
// 2. check if the embedding is older than the snapshot and update
|
||||
// 3. check if the embedding is older than 10 minutes (avoid frequent updates)
|
||||
// if all conditions are met, re-embedding is required.
|
||||
// 1. first-time embedding when no embedding exists
|
||||
// 2. re-embedding only when the doc has updates newer than the last embedding
|
||||
// AND the last embedding is older than 10 minutes (avoid frequent updates)
|
||||
const result = await this.db.$queryRaw<{ needs_embedding: boolean }[]>`
|
||||
SELECT
|
||||
EXISTS (
|
||||
@@ -280,8 +279,7 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
AND e.doc_id = docs.doc_id
|
||||
WHERE
|
||||
e.updated_at IS NULL
|
||||
OR docs.updated_at > e.updated_at
|
||||
OR e.updated_at < NOW() - INTERVAL '10 minutes'
|
||||
OR (docs.updated_at > e.updated_at AND e.updated_at < NOW() - INTERVAL '10 minutes')
|
||||
) AS needs_embedding;
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user