mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
feat(nbstore): optimize search performance (#11778)
now we can debug indexeddb performance by devtool 
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -52,7 +52,9 @@ export class DocsSearchService extends Service {
|
||||
occur: 'should',
|
||||
queries: [
|
||||
{
|
||||
type: 'all',
|
||||
type: 'match',
|
||||
field: 'content',
|
||||
match: query,
|
||||
},
|
||||
{
|
||||
type: 'boost',
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { truncate } from 'lodash-es';
|
||||
import { EMPTY, map, mergeMap, of, switchMap } from 'rxjs';
|
||||
import { EMPTY, map, mergeMap, of, switchMap, throttleTime } from 'rxjs';
|
||||
|
||||
import type { DocRecord, DocsService } from '../../doc';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
@@ -50,6 +50,10 @@ export class DocsQuickSearchSession
|
||||
items$ = new LiveData<QuickSearchItem<'docs', DocsPayload>[]>([]);
|
||||
|
||||
query = effect(
|
||||
throttleTime<string>(1000, undefined, {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
}),
|
||||
switchMap((query: string) => {
|
||||
let out;
|
||||
if (!query) {
|
||||
|
||||
@@ -143,43 +143,42 @@ export class SearchMenuService extends Service {
|
||||
// only search docs by title, excluding blocks
|
||||
private searchDocs$(query: string) {
|
||||
return this.docsSearch.indexer
|
||||
.aggregate$(
|
||||
.search$(
|
||||
'doc',
|
||||
{
|
||||
type: 'boolean',
|
||||
occur: 'must',
|
||||
queries: [
|
||||
type: 'match',
|
||||
field: 'title',
|
||||
match: query,
|
||||
},
|
||||
{
|
||||
fields: ['docId', 'title'],
|
||||
pagination: {
|
||||
limit: 1,
|
||||
},
|
||||
highlights: [
|
||||
{
|
||||
type: 'match',
|
||||
field: 'title',
|
||||
match: query,
|
||||
before: `<span style="color: ${cssVarV2('text/emphasis')}">`,
|
||||
end: '</span>',
|
||||
},
|
||||
],
|
||||
},
|
||||
'docId',
|
||||
{
|
||||
hits: {
|
||||
fields: ['docId', 'title'],
|
||||
pagination: {
|
||||
limit: 1,
|
||||
},
|
||||
highlights: [
|
||||
{
|
||||
field: 'title',
|
||||
before: `<span style="color: ${cssVarV2('text/emphasis')}">`,
|
||||
end: '</span>',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
.pipe(
|
||||
map(({ buckets }) =>
|
||||
buckets.map(bucket => {
|
||||
map(({ nodes }) =>
|
||||
nodes.map(node => {
|
||||
const id =
|
||||
typeof node.fields.docId === 'string'
|
||||
? node.fields.docId
|
||||
: node.fields.docId[0];
|
||||
const title =
|
||||
typeof node.fields.title === 'string'
|
||||
? node.fields.title
|
||||
: node.fields.title[0];
|
||||
return {
|
||||
id: bucket.key,
|
||||
title: bucket.hits.nodes[0].fields.title,
|
||||
highlights: bucket.hits.nodes[0].highlights.title[0],
|
||||
id,
|
||||
title,
|
||||
highlights: node.highlights.title[0],
|
||||
};
|
||||
})
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user