feat(server): add search docs by keyword gql api (#12866)

close AI-220









#### PR Dependency Tree


* **PR #12866** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new document search capability, allowing users to search
for documents by keyword within a workspace.
- Search results include document details such as title, highlights,
creation and update timestamps, and creator/updater information.
  - Added support for limiting the number of search results returned.
- **Tests**
- Added comprehensive end-to-end and snapshot tests to ensure accuracy
and access control for the new search functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-06-20 19:29:37 +08:00
committed by GitHub
parent 10e981aa6d
commit 011f92f7da
9 changed files with 430 additions and 0 deletions
@@ -0,0 +1,51 @@
# Snapshot report for `src/__tests__/e2e/indexer/search-docs.spec.ts`
The actual snapshot is saved in `search-docs.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should search docs by keyword
> Snapshot 1
[
{
blockId: 'block-0',
createdAt: '2025-04-22T00:00:00.000Z',
docId: 'doc-0',
highlight: 'test1 <b>hello</b>',
title: '',
updatedAt: '2025-04-22T00:00:00.000Z',
},
{
blockId: 'block-2',
createdAt: '2025-03-22T00:00:00.000Z',
docId: 'doc-2',
highlight: 'test3 <b>hello</b>',
title: '',
updatedAt: '2025-03-22T03:00:01.000Z',
},
{
blockId: 'block-1',
createdAt: '2021-04-22T00:00:00.000Z',
docId: 'doc-1',
highlight: 'test2 <b>hello</b>',
title: '',
updatedAt: '2021-04-22T00:00:00.000Z',
},
]
## should search docs by keyword with limit 1
> Snapshot 1
[
{
blockId: 'block-0',
createdAt: '2025-04-22T00:00:00.000Z',
docId: 'doc-0',
highlight: 'test1 <b>hello</b>',
title: '',
updatedAt: '2025-04-22T00:00:00.000Z',
},
]
@@ -0,0 +1,182 @@
import { indexerSearchDocsQuery, SearchTable } from '@affine/graphql';
import { omit } from 'lodash-es';
import { IndexerService } from '../../../plugins/indexer/service';
import { Mockers } from '../../mocks';
import { app, e2e } from '../test';
e2e('should search docs by keyword', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner,
});
const indexerService = app.get(IndexerService);
await indexerService.write(
SearchTable.block,
[
{
docId: 'doc-0',
workspaceId: workspace.id,
content: 'test1 hello',
flavour: 'markdown',
blockId: 'block-0',
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-04-22T00:00:00.000Z'),
updatedAt: new Date('2025-04-22T00:00:00.000Z'),
},
{
docId: 'doc-1',
workspaceId: workspace.id,
content: 'test2 hello',
flavour: 'markdown',
blockId: 'block-1',
refDocId: ['doc-0'],
ref: ['{"foo": "bar1"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2021-04-22T00:00:00.000Z'),
updatedAt: new Date('2021-04-22T00:00:00.000Z'),
},
{
docId: 'doc-2',
workspaceId: workspace.id,
content: 'test3 hello',
flavour: 'markdown',
blockId: 'block-2',
refDocId: ['doc-0', 'doc-2'],
ref: ['{"foo": "bar1"}', '{"foo": "bar3"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-03-22T00:00:00.000Z'),
updatedAt: new Date('2025-03-22T03:00:01.000Z'),
},
],
{
refresh: true,
}
);
const result = await app.gql({
query: indexerSearchDocsQuery,
variables: {
id: workspace.id,
input: {
keyword: 'hello',
},
},
});
t.is(result.workspace.searchDocs.length, 3);
t.snapshot(
result.workspace.searchDocs.map(doc =>
omit(doc, 'createdByUser', 'updatedByUser')
)
);
});
e2e('should search docs by keyword with limit 1', async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner,
});
const indexerService = app.get(IndexerService);
await indexerService.write(
SearchTable.block,
[
{
docId: 'doc-0',
workspaceId: workspace.id,
content: 'test1 hello',
flavour: 'markdown',
blockId: 'block-0',
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-04-22T00:00:00.000Z'),
updatedAt: new Date('2025-04-22T00:00:00.000Z'),
},
{
docId: 'doc-1',
workspaceId: workspace.id,
content: 'test2 hello',
flavour: 'markdown',
blockId: 'block-1',
refDocId: ['doc-0'],
ref: ['{"foo": "bar1"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2021-04-22T00:00:00.000Z'),
updatedAt: new Date('2021-04-22T00:00:00.000Z'),
},
{
docId: 'doc-2',
workspaceId: workspace.id,
content: 'test3 hello',
flavour: 'markdown',
blockId: 'block-2',
refDocId: ['doc-0', 'doc-2'],
ref: ['{"foo": "bar1"}', '{"foo": "bar3"}'],
createdByUserId: owner.id,
updatedByUserId: owner.id,
createdAt: new Date('2025-03-22T00:00:00.000Z'),
updatedAt: new Date('2025-03-22T03:00:01.000Z'),
},
],
{
refresh: true,
}
);
const result = await app.gql({
query: indexerSearchDocsQuery,
variables: {
id: workspace.id,
input: {
keyword: 'hello',
limit: 1,
},
},
});
t.is(result.workspace.searchDocs.length, 1);
t.snapshot(
result.workspace.searchDocs.map(doc =>
omit(doc, 'createdByUser', 'updatedByUser')
)
);
});
e2e(
'should search docs by keyword failed when workspace is no permission',
async t => {
const owner = await app.signup();
const workspace = await app.create(Mockers.Workspace, {
owner,
});
// signup another user
await app.signup();
await t.throwsAsync(
app.gql({
query: indexerSearchDocsQuery,
variables: {
id: workspace.id,
input: {
keyword: 'hello',
},
},
}),
{
message: /You do not have permission to access Space/,
}
);
}
);