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:
DarkSky
2025-08-12 09:45:41 +08:00
committed by GitHub
parent 125564b7d2
commit 65f679c4f0
10 changed files with 177 additions and 43 deletions
@@ -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;
`;