Merge branch 'move-author-search-to-author-stats' into 'main'

Order search by followers count & Move author search to author stats

Closes #140

See merge request soapbox-pub/ditto!500
This commit is contained in:
Alex Gleason 2024-09-20 13:57:31 +00:00
commit ab727c3940
8 changed files with 64 additions and 15 deletions

View file

@ -9,7 +9,6 @@ export interface DittoTables extends NPostgresSchema {
event_stats: EventStatsRow; event_stats: EventStatsRow;
pubkey_domains: PubkeyDomainRow; pubkey_domains: PubkeyDomainRow;
event_zaps: EventZapRow; event_zaps: EventZapRow;
author_search: AuthorSearch;
} }
type NostrEventsRow = NPostgresSchema['nostr_events'] & { type NostrEventsRow = NPostgresSchema['nostr_events'] & {
@ -21,6 +20,7 @@ interface AuthorStatsRow {
followers_count: number; followers_count: number;
following_count: number; following_count: number;
notes_count: number; notes_count: number;
search: string;
} }
interface EventStatsRow { interface EventStatsRow {
@ -55,8 +55,3 @@ interface EventZapRow {
amount_millisats: number; amount_millisats: number;
comment: string; comment: string;
} }
interface AuthorSearch {
pubkey: string;
search: string;
}

View file

@ -0,0 +1,32 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('author_stats')
.addColumn('search', 'text', (col) => col.notNull().defaultTo(''))
.execute();
await sql`CREATE INDEX author_stats_search_idx ON author_stats USING GIN (search gin_trgm_ops)`.execute(db);
await db.insertInto('author_stats')
.columns(['pubkey', 'search'])
.expression(
db.selectFrom('author_search')
.select(['pubkey', 'search']),
)
.onConflict((oc) =>
oc.column('pubkey')
.doUpdateSet((eb) => ({
search: eb.ref('excluded.search'),
}))
)
.execute();
await db.schema.dropIndex('author_search_search_idx').ifExists().execute();
await db.schema.dropTable('author_search').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropIndex('author_stats_search_idx').ifExists().execute();
await db.schema.alterTable('author_stats').dropColumn('search').execute();
}

View file

@ -157,8 +157,8 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise<vo
const search = result?.pubkey === event.pubkey ? [name, nip05].filter(Boolean).join(' ').trim() : name ?? ''; const search = result?.pubkey === event.pubkey ? [name, nip05].filter(Boolean).join(' ').trim() : name ?? '';
if (search) { if (search) {
await kysely.insertInto('author_search') await kysely.insertInto('author_stats')
.values({ pubkey: event.pubkey, search }) .values({ pubkey: event.pubkey, search, followers_count: 0, following_count: 0, notes_count: 0 })
.onConflict((oc) => oc.column('pubkey').doUpdateSet({ search })) .onConflict((oc) => oc.column('pubkey').doUpdateSet({ search }))
.execute(); .execute();
} }

View file

@ -303,6 +303,7 @@ async function gatherAuthorStats(
followers_count: Math.max(0, row.followers_count), followers_count: Math.max(0, row.followers_count),
following_count: Math.max(0, row.following_count), following_count: Math.max(0, row.following_count),
notes_count: Math.max(0, row.notes_count), notes_count: Math.max(0, row.notes_count),
search: row.search,
})); }));
} }

View file

@ -6,9 +6,12 @@ import { getPubkeysBySearch } from '@/utils/search.ts';
Deno.test('fuzzy search works', async () => { Deno.test('fuzzy search works', async () => {
await using db = await createTestDB(); await using db = await createTestDB();
await db.kysely.insertInto('author_search').values({ await db.kysely.insertInto('author_stats').values({
pubkey: '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', pubkey: '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4',
search: 'patrickReiis patrickdosreis.com', search: 'patrickReiis patrickdosreis.com',
notes_count: 0,
followers_count: 0,
following_count: 0,
}).execute(); }).execute();
assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followedPubkeys: new Set() }), new Set()); assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followedPubkeys: new Set() }), new Set());

View file

@ -10,13 +10,14 @@ export async function getPubkeysBySearch(
const { q, limit, followedPubkeys } = opts; const { q, limit, followedPubkeys } = opts;
let query = kysely let query = kysely
.selectFrom('author_search') .selectFrom('author_stats')
.select((eb) => [ .select((eb) => [
'pubkey', 'pubkey',
'search', 'search',
eb.fn('word_similarity', [sql`${q}`, 'search']).as('sml'), eb.fn('word_similarity', [sql`${q}`, 'search']).as('sml'),
]) ])
.where(() => sql`${q} <% search`) .where(() => sql`${q} <% search`)
.orderBy(['followers_count desc'])
.orderBy(['sml desc', 'search']) .orderBy(['sml desc', 'search'])
.limit(limit); .limit(limit);

View file

@ -171,7 +171,16 @@ Deno.test('countAuthorStats counts author stats from the database', async () =>
await db.store.event(genEvent({ kind: 1, content: 'yolo' }, sk)); await db.store.event(genEvent({ kind: 1, content: 'yolo' }, sk));
await db.store.event(genEvent({ kind: 3, tags: [['p', pubkey]] })); await db.store.event(genEvent({ kind: 3, tags: [['p', pubkey]] }));
const stats = await countAuthorStats(db.store, pubkey); await db.kysely.insertInto('author_stats').values({
pubkey,
search: 'Yolo Lolo',
notes_count: 0,
followers_count: 0,
following_count: 0,
}).onConflict((oc) => oc.column('pubkey').doUpdateSet({ 'search': 'baka' }))
.execute();
const stats = await countAuthorStats({ store: db.store, pubkey, kysely: db.kysely });
assertEquals(stats!.notes_count, 2); assertEquals(stats!.notes_count, 2);
assertEquals(stats!.followers_count, 1); assertEquals(stats!.followers_count, 1);

View file

@ -194,6 +194,7 @@ export async function updateAuthorStats(
followers_count: 0, followers_count: 0,
following_count: 0, following_count: 0,
notes_count: 0, notes_count: 0,
search: '',
}; };
const prev = await kysely const prev = await kysely
@ -268,20 +269,27 @@ export async function updateEventStats(
/** Calculate author stats from the database. */ /** Calculate author stats from the database. */
export async function countAuthorStats( export async function countAuthorStats(
store: SetRequired<NStore, 'count'>, { pubkey, store }: RefreshAuthorStatsOpts,
pubkey: string,
): Promise<DittoTables['author_stats']> { ): Promise<DittoTables['author_stats']> {
const [{ count: followers_count }, { count: notes_count }, [followList]] = await Promise.all([ const [{ count: followers_count }, { count: notes_count }, [followList], [kind0]] = await Promise.all([
store.count([{ kinds: [3], '#p': [pubkey] }]), store.count([{ kinds: [3], '#p': [pubkey] }]),
store.count([{ kinds: [1], authors: [pubkey] }]), store.count([{ kinds: [1], authors: [pubkey] }]),
store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]), store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]),
store.query([{ kinds: [0], authors: [pubkey], limit: 1 }]),
]); ]);
let search: string = '';
const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(kind0?.content);
if (metadata.success) {
const { name, nip05 } = metadata.data;
search = [name, nip05].filter(Boolean).join(' ').trim();
}
return { return {
pubkey, pubkey,
followers_count, followers_count,
following_count: getTagSet(followList?.tags ?? [], 'p').size, following_count: getTagSet(followList?.tags ?? [], 'p').size,
notes_count, notes_count,
search,
}; };
} }
@ -295,7 +303,7 @@ export interface RefreshAuthorStatsOpts {
export async function refreshAuthorStats( export async function refreshAuthorStats(
{ pubkey, kysely, store }: RefreshAuthorStatsOpts, { pubkey, kysely, store }: RefreshAuthorStatsOpts,
): Promise<DittoTables['author_stats']> { ): Promise<DittoTables['author_stats']> {
const stats = await countAuthorStats(store, pubkey); const stats = await countAuthorStats({ store, pubkey, kysely });
await kysely.insertInto('author_stats') await kysely.insertInto('author_stats')
.values(stats) .values(stats)