From 62d74de81011e081c1b66b37774f2bdc46b8fcf5 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Fri, 20 Jun 2025 17:48:30 +0800 Subject: [PATCH] feat(server): search docs by keywork from indexer (#12863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #12867** * **PR #12863** 👈 * **PR #12837** * **PR #12866** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) --- packages/backend/server/src/models/user.ts | 17 ++ .../__snapshots__/service.spec.ts.md | 35 ++++ .../__snapshots__/service.spec.ts.snap | Bin 3946 -> 4273 bytes .../plugins/indexer/__tests__/service.spec.ts | 98 ++++++++++++ .../server/src/plugins/indexer/index.ts | 1 + .../server/src/plugins/indexer/service.ts | 150 ++++++++++++++++++ .../server/src/plugins/indexer/types.ts | 14 ++ 7 files changed, 315 insertions(+) diff --git a/packages/backend/server/src/models/user.ts b/packages/backend/server/src/models/user.ts index f86a49e450..dd69600d74 100644 --- a/packages/backend/server/src/models/user.ts +++ b/packages/backend/server/src/models/user.ts @@ -45,6 +45,10 @@ interface UserFilter { withDisabled?: boolean; } +export interface ItemWithUserId { + userId: string; +} + export type PublicUser = Pick; export type WorkspaceUser = Pick; export type { ConnectedAccount, User }; @@ -78,6 +82,19 @@ export class UserModel extends BaseModel { }); } + async getPublicUsersMap( + items: T[] + ): Promise> { + const userIds: string[] = []; + for (const item of items) { + if (item.userId) { + userIds.push(item.userId); + } + } + const users = await this.getPublicUsers(userIds); + return new Map(users.map(user => [user.id, user])); + } + async getWorkspaceUser(id: string): Promise { return this.db.user.findUnique({ select: workspaceUserSelect, diff --git a/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.md b/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.md index 91c2ab9488..57518ce9bd 100644 --- a/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.md +++ b/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.md @@ -521,3 +521,38 @@ Generated by [AVA](https://avajs.dev). 'blob3 name.docx', ], ] + +## should search docs by keyword work + +> Snapshot 1 + + [ + { + blockId: 'block1', + createdAt: Date 2025-06-20 00:00:00 UTC {}, + highlight: 'hello world', + title: 'hello world', + updatedAt: Date 2025-06-20 00:00:00 UTC {}, + }, + { + blockId: 'block2', + createdAt: Date 2025-06-20 00:00:01 UTC {}, + highlight: 'hello world 2', + title: 'hello world 2', + updatedAt: Date 2025-06-20 00:00:01 UTC {}, + }, + { + blockId: 'block3', + createdAt: Date 2025-06-20 00:00:02 UTC {}, + highlight: 'hello world 3', + title: 'hello world 3', + updatedAt: Date 2025-06-20 00:00:02 UTC {}, + }, + { + blockId: 'block4', + createdAt: Date 2025-06-20 00:00:03 UTC {}, + highlight: 'hello world 4', + title: '', + updatedAt: Date 2025-06-20 00:00:03 UTC {}, + }, + ] diff --git a/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.snap b/packages/backend/server/src/plugins/indexer/__tests__/__snapshots__/service.spec.ts.snap index 1c27fea9df625b78af491aa1dfb594b08ce9b13c..e3dcb5368ef3cf12d1b590dcf73a860ff0556943 100644 GIT binary patch literal 4273 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 literal 3946 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 E07QbFC;$Ke diff --git a/packages/backend/server/src/plugins/indexer/__tests__/service.spec.ts b/packages/backend/server/src/plugins/indexer/__tests__/service.spec.ts index 6759b2c014..f533499a54 100644 --- a/packages/backend/server/src/plugins/indexer/__tests__/service.spec.ts +++ b/packages/backend/server/src/plugins/indexer/__tests__/service.spec.ts @@ -2213,3 +2213,101 @@ test('should search blob names work', async t => { }); // #endregion + +// #region searchDocsByKeyword() + +test('should search docs by keyword work', async t => { + const workspaceId = workspace.id; + const docId1 = randomUUID(); + const docId2 = randomUUID(); + const docId3 = randomUUID(); + const docId4 = randomUUID(); + + await module.create(Mockers.DocMeta, { + workspaceId, + docId: docId1, + title: 'hello world 1', + }); + await module.create(Mockers.DocMeta, { + workspaceId, + docId: docId2, + title: 'hello world 2', + }); + await module.create(Mockers.DocMeta, { + workspaceId, + docId: docId3, + title: 'hello world 3', + }); + + await indexerService.write( + SearchTable.block, + [ + { + workspaceId, + docId: docId1, + blockId: 'block1', + content: 'hello world', + flavour: 'affine:page', + createdByUserId: user.id, + updatedByUserId: user.id, + createdAt: new Date('2025-06-20T00:00:00.000Z'), + updatedAt: new Date('2025-06-20T00:00:00.000Z'), + }, + { + workspaceId, + docId: docId2, + blockId: 'block2', + content: 'hello world 2', + flavour: 'affine:text', + createdByUserId: user.id, + updatedByUserId: user.id, + createdAt: new Date('2025-06-20T00:00:01.000Z'), + updatedAt: new Date('2025-06-20T00:00:01.000Z'), + }, + { + workspaceId, + docId: docId3, + blockId: 'block3', + content: 'hello world 3', + flavour: 'affine:text', + createdByUserId: user.id, + updatedByUserId: user.id, + createdAt: new Date('2025-06-20T00:00:02.000Z'), + updatedAt: new Date('2025-06-20T00:00:02.000Z'), + }, + { + workspaceId, + docId: docId4, + blockId: 'block4', + content: 'hello world 4', + flavour: 'affine:text', + createdByUserId: user.id, + updatedByUserId: user.id, + createdAt: new Date('2025-06-20T00:00:03.000Z'), + updatedAt: new Date('2025-06-20T00:00:03.000Z'), + }, + ], + { + refresh: true, + } + ); + + const rows = await indexerService.searchDocsByKeyword(workspaceId, 'hello'); + + t.is(rows.length, 4); + t.snapshot( + rows + .map(row => + omit(row, [ + 'docId', + 'createdByUserId', + 'updatedByUserId', + 'createdByUser', + 'updatedByUser', + ]) + ) + .sort((a, b) => a.blockId.localeCompare(b.blockId)) + ); +}); + +// #endregion diff --git a/packages/backend/server/src/plugins/indexer/index.ts b/packages/backend/server/src/plugins/indexer/index.ts index c6e4ea15d4..4a012f8cab 100644 --- a/packages/backend/server/src/plugins/indexer/index.ts +++ b/packages/backend/server/src/plugins/indexer/index.ts @@ -26,6 +26,7 @@ import { IndexerService } from './service'; export class IndexerModule {} export { IndexerService }; +export type { SearchDoc } from './types'; declare global { interface Events { diff --git a/packages/backend/server/src/plugins/indexer/service.ts b/packages/backend/server/src/plugins/indexer/service.ts index d0fd67ba82..a8c4013699 100644 --- a/packages/backend/server/src/plugins/indexer/service.ts +++ b/packages/backend/server/src/plugins/indexer/service.ts @@ -33,6 +33,7 @@ import { } from './tables'; import { AggregateInput, + SearchDoc, SearchHighlight, SearchInput, SearchQuery, @@ -433,6 +434,155 @@ export class IndexerService { return blobNameMap; } + async searchDocsByKeyword( + workspaceId: string, + keyword: string, + options?: { + limit?: number; + } + ): Promise { + const limit = options?.limit ?? 20; + const result = await this.aggregate({ + table: SearchTable.block, + field: 'docId', + query: { + type: SearchQueryType.boolean, + occur: SearchQueryOccur.must, + queries: [ + { + type: SearchQueryType.match, + field: 'workspaceId', + match: workspaceId, + }, + { + type: SearchQueryType.boolean, + occur: SearchQueryOccur.must, + queries: [ + { + type: SearchQueryType.match, + field: 'content', + match: keyword, + }, + { + type: SearchQueryType.boolean, + occur: SearchQueryOccur.should, + queries: [ + { + type: SearchQueryType.match, + field: 'content', + match: keyword, + }, + { + type: SearchQueryType.boost, + boost: 1.5, + query: { + type: SearchQueryType.match, + field: 'flavour', + match: 'affine:page', + }, + }, + ], + }, + ], + }, + ], + }, + options: { + hits: { + fields: [ + 'blockId', + 'flavour', + 'content', + 'createdAt', + 'updatedAt', + 'createdByUserId', + 'updatedByUserId', + ], + highlights: [ + { + field: 'content', + before: '', + end: '', + }, + ], + pagination: { + limit: 2, + }, + }, + pagination: { + limit, + }, + }, + }); + + const docs: SearchDoc[] = []; + const missingTitles: { workspaceId: string; docId: string }[] = []; + const userIds: { userId: string }[] = []; + + for (const bucket of result.buckets) { + const docId = bucket.key; + const blockId = bucket.hits.nodes[0].fields.blockId[0] as string; + const flavour = bucket.hits.nodes[0].fields.flavour[0] as string; + const content = bucket.hits.nodes[0].fields.content[0] as string; + const createdAt = bucket.hits.nodes[0].fields.createdAt[0] as Date; + const updatedAt = bucket.hits.nodes[0].fields.updatedAt[0] as Date; + const createdByUserId = bucket.hits.nodes[0].fields + .createdByUserId[0] as string; + const updatedByUserId = bucket.hits.nodes[0].fields + .updatedByUserId[0] as string; + const highlight = bucket.hits.nodes[0].highlights?.content?.[0] as string; + let title = ''; + + // hit title block + if (flavour === 'affine:page') { + title = content; + } else { + // hit content block, missing title + missingTitles.push({ workspaceId, docId }); + } + + docs.push({ + docId, + blockId, + title, + highlight, + createdAt, + updatedAt, + createdByUserId, + updatedByUserId, + }); + userIds.push({ userId: createdByUserId }, { userId: updatedByUserId }); + } + + if (missingTitles.length > 0) { + const metas = await this.models.doc.findMetas(missingTitles, { + select: { + title: true, + }, + }); + const titleMap = new Map(); + for (const meta of metas) { + if (meta?.title) { + titleMap.set(meta.docId, meta.title); + } + } + for (const doc of docs) { + if (!doc.title) { + doc.title = titleMap.get(doc.docId) ?? ''; + } + } + } + + const userMap = await this.models.user.getPublicUsersMap(userIds); + + for (const doc of docs) { + doc.createdByUser = userMap.get(doc.createdByUserId); + doc.updatedByUser = userMap.get(doc.updatedByUserId); + } + + return docs; + } + #formatSearchNodes(nodes: SearchNode[]) { return nodes.map(node => ({ ...node, diff --git a/packages/backend/server/src/plugins/indexer/types.ts b/packages/backend/server/src/plugins/indexer/types.ts index e58b071b5c..1d20f3c845 100644 --- a/packages/backend/server/src/plugins/indexer/types.ts +++ b/packages/backend/server/src/plugins/indexer/types.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/graphql'; import { GraphQLJSONObject } from 'graphql-scalars'; +import { PublicUser } from '../../models'; import { SearchTable } from './tables'; export enum SearchQueryType { @@ -40,6 +41,19 @@ registerEnumType(SearchQueryOccur, { description: 'Search query occur', }); +export interface SearchDoc { + docId: string; + blockId: string; + title: string; + highlight: string; + createdAt: Date; + updatedAt: Date; + createdByUserId: string; + updatedByUserId: string; + createdByUser?: PublicUser; + updatedByUser?: PublicUser; +} + @InputType() export class SearchQuery { @Field(() => SearchQueryType)