feat(nbstore): optimize search performance (#11778)

now we can debug indexeddb performance by devtool

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/g3jz87HxbjOJpXV3FPT7/75cf686f-9bc4-4db3-86ac-12bba83e8d83.png)
This commit is contained in:
EYHN
2025-04-17 13:16:18 +00:00
parent 75df27a145
commit 5e9ad634b7
5 changed files with 116 additions and 33 deletions

View File

@@ -10,6 +10,7 @@ import {
type SearchOptions,
type SearchResult,
} from '../../../storage';
import { shallowEqual } from '../../../utils/shallow-equal';
import type { DocStorageSchema } from '../schema';
import { highlighter } from './highlighter';
import {
@@ -33,6 +34,8 @@ export type DataStructROTransaction = IDBPTransaction<
'readonly' | 'readwrite'
>;
let debugMarkCount = 0;
export class DataStruct {
database: IDBPDatabase<DocStorageSchema> = null as any;
invertedIndex = new Map<string, Map<string, InvertedIndex>>();
@@ -82,6 +85,7 @@ export class DataStruct {
table: keyof IndexerSchema,
document: IndexerDocument
) {
using _ = await this.measure(`update`);
const existsNid = await trx
.objectStore('indexerRecords')
.index('id')
@@ -119,6 +123,7 @@ export class DataStruct {
if (!iidx) {
continue;
}
using _ = await this.measure(`insert[${typeInfo.type}]`);
await iidx.insert(trx, nid, values);
}
}
@@ -129,6 +134,7 @@ export class DataStruct {
table: keyof IndexerSchema,
document: IndexerDocument
) {
using _ = await this.measure(`insert`);
const existsNid = await trx
.objectStore('indexerRecords')
.index('id')
@@ -160,6 +166,7 @@ export class DataStruct {
if (!iidx) {
continue;
}
using _ = await this.measure(`insert[${typeInfo.type}]`);
await iidx.insert(trx, nid, values);
}
}
@@ -183,6 +190,7 @@ export class DataStruct {
table: keyof IndexerSchema,
id: string
) {
using _ = await this.measure(`delete`);
const nid = await trx
.objectStore('indexerRecords')
.index('id')
@@ -195,11 +203,12 @@ export class DataStruct {
}
}
async deleteByQuery(
private async deleteByQuery(
trx: DataStructRWTransaction,
table: keyof IndexerSchema,
query: Query<any>
) {
using _ = await this.measure(`deleteByQuery`);
const match = await this.queryRaw(trx, table, query);
for (const nid of match.scores.keys()) {
@@ -215,6 +224,7 @@ export class DataStruct {
inserts: IndexerDocument<any>[],
updates: IndexerDocument<any>[]
) {
using _ = await this.measure(`batchWrite`);
for (const query of deleteByQueries) {
await this.deleteByQuery(trx, table, query);
}
@@ -248,8 +258,14 @@ export class DataStruct {
async queryRaw(
trx: DataStructROTransaction,
table: keyof IndexerSchema,
query: Query<any>
query: Query<any>,
cache: QueryCache = new QueryCache()
): Promise<Match> {
const cached = cache.get(query);
if (cached) {
return cached;
}
using _ = await this.measure(`query[${query.type}]`);
if (query.type === 'match') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
@@ -259,7 +275,7 @@ export class DataStruct {
} else if (query.type === 'boolean') {
const weights = [];
for (const q of query.queries) {
weights.push(await this.queryRaw(trx, table, q));
weights.push(await this.queryRaw(trx, table, q, cache));
}
if (query.occur === 'must') {
return weights.reduce((acc, w) => acc.and(w));
@@ -272,7 +288,9 @@ export class DataStruct {
} else if (query.type === 'all') {
return await this.matchAll(trx, table);
} else if (query.type === 'boost') {
return (await this.queryRaw(trx, table, query.query)).boost(query.boost);
return (await this.queryRaw(trx, table, query.query, cache)).boost(
query.boost
);
} else if (query.type === 'exists') {
const iidx = this.invertedIndex.get(table)?.get(query.field as string);
if (!iidx) {
@@ -490,4 +508,30 @@ export class DataStruct {
return node;
}
async measure(name: string) {
const count = debugMarkCount++;
performance.mark(`${name}Start(${count})`);
return {
[Symbol.dispose]: () => {
performance.mark(`${name}End(${count})`);
performance.measure(
`${name}`,
`${name}Start(${count})`,
`${name}End(${count})`
);
},
};
}
}
class QueryCache {
private readonly cache: [Query<any>, Match][] = [];
get(query: Query<any>) {
return this.cache.find(q => shallowEqual(q[0], query))?.[1];
}
set(query: Query<any>, match: Match) {
this.cache.push([query, match]);
}
}

View File

@@ -0,0 +1,34 @@
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
export function shallowEqual(objA: any, objB: any) {
if (Object.is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (const key of keysA) {
if (
!Object.prototype.hasOwnProperty.call(objB, key) ||
!Object.is(objA[key], objB[key])
) {
return false;
}
}
return true;
}