mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
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:
commit
ab727c3940
8 changed files with 64 additions and 15 deletions
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
32
src/db/migrations/034_move_author_search_to_author_stats.ts
Normal file
32
src/db/migrations/034_move_author_search_to_author_stats.ts
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue