feat(server): search blob names from indexer (#12822)

#### PR Dependency Tree


* **PR #12822** 👈

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**
- Added the ability to search for blob names by their IDs within a
workspace.
- **Tests**
- Introduced new test cases and snapshot tests to validate searching
blob names and reading filenames from document snapshots.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-06-16 16:34:23 +08:00
committed by GitHub
parent 4f75111055
commit c0c813edfd
9 changed files with 282 additions and 1 deletions
@@ -494,3 +494,30 @@ Generated by [AVA](https://avajs.dev).
],
},
]
## should search blob names from doc snapshot work
> Snapshot 1
Map {
'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=' => 'test file name here.txt',
}
## should search blob names work
> Snapshot 1
[
[
'blob1',
'blob1 name.txt',
],
[
'blob2',
'blob2 name.md',
],
[
'blob3',
'blob3 name.docx',
],
]
@@ -2113,3 +2113,103 @@ test('should index doc work', async t => {
t.is(module.event.count('doc.indexer.updated'), count + 1);
});
// #endregion
// #region searchBlobNames()
test('should search blob names from doc snapshot work', async t => {
const docSnapshot = await module.create(Mockers.DocSnapshot, {
workspaceId: workspace.id,
user,
snapshotFile: 'test-doc-with-blob.snapshot.bin',
});
await indexerService.indexDoc(workspace.id, docSnapshot.id, {
refresh: true,
});
const blobNameMap = await indexerService.searchBlobNames(workspace.id, [
'ldZMrM4PDlsNG4Q4YvCsz623h6TKu4qI9_FpTqIypfw=',
]);
t.snapshot(blobNameMap);
});
test('should search blob names work', async t => {
const workspaceId = randomUUID();
const blobId1 = 'blob1';
const blobId2 = 'blob2';
const blobId3 = 'blob3';
const blobId4 = 'blob4';
await indexerService.write(
SearchTable.block,
[
{
workspaceId,
blob: blobId1,
content: 'blob1 name.txt',
flavour: 'affine:attachment',
docId: randomUUID(),
blockId: randomUUID(),
createdByUserId: user.id,
updatedByUserId: user.id,
createdAt: new Date(),
updatedAt: new Date(),
},
{
workspaceId,
blob: blobId2,
content: 'blob2 name.md',
flavour: 'affine:attachment',
docId: randomUUID(),
blockId: randomUUID(),
createdByUserId: user.id,
updatedByUserId: user.id,
createdAt: new Date(),
updatedAt: new Date(),
},
{
workspaceId,
blob: blobId3,
content: 'blob3 name.docx',
flavour: 'affine:attachment',
docId: randomUUID(),
blockId: randomUUID(),
createdByUserId: user.id,
updatedByUserId: user.id,
createdAt: new Date(),
updatedAt: new Date(),
},
// no attachment
{
workspaceId,
blob: blobId3,
content: 'mock blob3 content',
flavour: 'affine:page',
docId: randomUUID(),
blockId: randomUUID(),
createdByUserId: user.id,
updatedByUserId: user.id,
createdAt: new Date(),
updatedAt: new Date(),
},
],
{
refresh: true,
}
);
const blobNameMap = await indexerService.searchBlobNames(workspaceId, [
blobId1,
blobId2,
blobId3,
blobId4,
]);
t.is(blobNameMap.size, 3);
t.snapshot(
Array.from(blobNameMap.entries()).sort((a, b) => a[0].localeCompare(b[0]))
);
});
// #endregion
@@ -387,6 +387,52 @@ export class IndexerService {
await searchProvider.deleteByQuery(table, dsl, options);
}
async searchBlobNames(workspaceId: string, blobIds: string[]) {
const result = await this.search({
table: SearchTable.block,
query: {
type: SearchQueryType.boolean,
occur: SearchQueryOccur.must,
queries: [
{
type: SearchQueryType.match,
field: 'workspaceId',
match: workspaceId,
},
{
type: SearchQueryType.match,
field: 'flavour',
match: 'affine:attachment',
},
{
type: SearchQueryType.boolean,
occur: SearchQueryOccur.should,
queries: blobIds.map(blobId => ({
type: SearchQueryType.match,
field: 'blob',
match: blobId,
})),
},
],
},
options: {
fields: ['blob', 'content'],
pagination: {
limit: 10000,
},
},
});
const blobNameMap = new Map<string, string>();
for (const node of result.nodes) {
const blobId = node.fields.blob[0] as string;
const content = node.fields.content[0] as string;
if (blobId && content) {
blobNameMap.set(blobId, content);
}
}
return blobNameMap;
}
#formatSearchNodes(nodes: SearchNode[]) {
return nodes.map(node => ({
...node,