From 2fe6a8fde500f9351c8c8b011cd45be8b87da980 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 14:24:26 -0300 Subject: [PATCH 1/8] refactor: move getPubkeysBySearch () function to a new location --- src/controllers/api/accounts.ts | 2 +- src/controllers/api/search.ts | 17 ++--------------- src/{controllers/api => utils}/search.test.ts | 2 +- src/utils/search.ts | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 17 deletions(-) rename src/{controllers/api => utils}/search.test.ts (92%) create mode 100644 src/utils/search.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index c946b697..f14b8b5d 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -6,7 +6,6 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; -import { getPubkeysBySearch } from '@/controllers/api/search.ts'; import { Storages } from '@/storages.ts'; import { uploadFile } from '@/utils/upload.ts'; import { nostrNow } from '@/utils.ts'; @@ -19,6 +18,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; +import { getPubkeysBySearch } from '@/utils/search.ts'; const usernameSchema = z .string().min(1).max(30) diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 01fb6665..05d42044 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -1,10 +1,8 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; -import { Kysely, sql } from 'kysely'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { DittoTables } from '@/db/DittoTables.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; @@ -12,6 +10,7 @@ import { extractIdentifier, lookupPubkey } from '@/utils/lookup.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { getPubkeysBySearch } from '@/utils/search.ts'; const searchQuerySchema = z.object({ q: z.string().transform(decodeURIComponent), @@ -194,16 +193,4 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort return []; } -/** Get pubkeys whose name and NIP-05 is similar to 'q' */ -async function getPubkeysBySearch(kysely: Kysely, { q, limit }: Pick) { - const pubkeys = (await sql<{ pubkey: string }>` - SELECT *, word_similarity(${q}, search) AS sml - FROM author_search - WHERE ${q} % search - ORDER BY sml DESC, search LIMIT ${limit} - `.execute(kysely)).rows.map(({ pubkey }) => pubkey); - - return pubkeys; -} - -export { getPubkeysBySearch, searchController }; +export { searchController }; diff --git a/src/controllers/api/search.test.ts b/src/utils/search.test.ts similarity index 92% rename from src/controllers/api/search.test.ts rename to src/utils/search.test.ts index 2c5e91bd..a67c0662 100644 --- a/src/controllers/api/search.test.ts +++ b/src/utils/search.test.ts @@ -1,7 +1,7 @@ import { assertEquals } from '@std/assert'; import { createTestDB } from '@/test.ts'; -import { getPubkeysBySearch } from '@/controllers/api/search.ts'; +import { getPubkeysBySearch } from '@/utils/search.ts'; Deno.test('fuzzy search works', async () => { await using db = await createTestDB(); diff --git a/src/utils/search.ts b/src/utils/search.ts new file mode 100644 index 00000000..460c2525 --- /dev/null +++ b/src/utils/search.ts @@ -0,0 +1,16 @@ +import { Kysely, sql } from 'kysely'; + +import { DittoTables } from '@/db/DittoTables.ts'; + +/** Get pubkeys whose name and NIP-05 is similar to 'q' */ +export async function getPubkeysBySearch(kysely: Kysely, opts: { q: string; limit: number }) { + const { q, limit } = opts; + const pubkeys = (await sql<{ pubkey: string }>` + SELECT *, word_similarity(${q}, search) AS sml + FROM author_search + WHERE ${q} % search + ORDER BY sml DESC, search LIMIT ${limit} + `.execute(kysely)).rows.map(({ pubkey }) => pubkey); + + return pubkeys; +} From 52001373e03a5959e2dff2910d69996222f3410b Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 17 Sep 2024 11:04:27 -0300 Subject: [PATCH 2/8] feat: show users you follow first in search getPubkeysBySearch() function refactored to accept a followList argument --- src/controllers/api/accounts.ts | 7 ++++++- src/controllers/api/search.ts | 17 ++++++++++++----- src/utils/search.ts | 34 ++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index f14b8b5d..ae3c7e10 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -117,6 +117,7 @@ const accountSearchController: AppController = async (c) => { const { signal } = c.req.raw; const { limit } = c.get('pagination'); const kysely = await Storages.kysely(); + const viewerPubkey = await c.get('signer')?.getPublicKey(); const result = accountSearchQuerySchema.safeParse(c.req.query()); @@ -135,7 +136,11 @@ const accountSearchController: AppController = async (c) => { return c.json(pubkey ? [await accountFromPubkey(pubkey)] : []); } - const pubkeys = await getPubkeysBySearch(kysely, { q: query, limit }); + const followList: string[] = []; + if (viewerPubkey) { + followList.push(...await getFollowedPubkeys(viewerPubkey)); + } + const pubkeys = (await getPubkeysBySearch(kysely, { q: query, limit, followList })).slice(0, limit); let events = event ? [event] : await store.query([{ kinds: [0], authors: pubkeys, limit }], { signal, diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 05d42044..ebcbc4ca 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -10,6 +10,7 @@ import { extractIdentifier, lookupPubkey } from '@/utils/lookup.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { getFollowedPubkeys } from '@/queries.ts'; import { getPubkeysBySearch } from '@/utils/search.ts'; const searchQuerySchema = z.object({ @@ -26,6 +27,7 @@ type SearchQuery = z.infer; const searchController: AppController = async (c) => { const result = searchQuerySchema.safeParse(c.req.query()); const { signal } = c.req.raw; + const viewerPubkey = await c.get('signer')?.getPublicKey(); if (!result.success) { return c.json({ error: 'Bad request', schema: result.error }, 422); @@ -49,9 +51,7 @@ const searchController: AppController = async (c) => { if (event) { events = [event]; } - events.push(...(await searchEvents(result.data, signal))); - - const viewerPubkey = await c.get('signer')?.getPublicKey(); + events.push(...(await searchEvents({ ...result.data, viewerPubkey }, signal))); const [accounts, statuses] = await Promise.all([ Promise.all( @@ -76,7 +76,10 @@ const searchController: AppController = async (c) => { }; /** Get events for the search params. */ -async function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise { +async function searchEvents( + { q, type, limit, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string }, + signal: AbortSignal, +): Promise { if (type === 'hashtags') return Promise.resolve([]); const filter: NostrFilter = { @@ -93,7 +96,11 @@ async function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: if (type === 'accounts') { const kysely = await Storages.kysely(); - pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit }))); + const followList: string[] = []; + if (viewerPubkey) { + followList.push(...await getFollowedPubkeys(viewerPubkey)); + } + pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit, followList }))); if (!filter?.authors) { filter.authors = pubkeys; diff --git a/src/utils/search.ts b/src/utils/search.ts index 460c2525..81b9240a 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -3,14 +3,30 @@ import { Kysely, sql } from 'kysely'; import { DittoTables } from '@/db/DittoTables.ts'; /** Get pubkeys whose name and NIP-05 is similar to 'q' */ -export async function getPubkeysBySearch(kysely: Kysely, opts: { q: string; limit: number }) { - const { q, limit } = opts; - const pubkeys = (await sql<{ pubkey: string }>` - SELECT *, word_similarity(${q}, search) AS sml - FROM author_search - WHERE ${q} % search - ORDER BY sml DESC, search LIMIT ${limit} - `.execute(kysely)).rows.map(({ pubkey }) => pubkey); +export async function getPubkeysBySearch( + kysely: Kysely, + opts: { q: string; limit: number; followList: string[] }, +) { + const { q, limit, followList } = opts; - return pubkeys; + let query = kysely + .selectFrom('author_search') + .select((eb) => [ + 'pubkey', + 'search', + eb.fn('word_similarity', [sql`${q}`, 'search']).as('sml'), + ]) + .where(() => sql`${q} % search`) + .orderBy(['sml desc', 'search']) + .limit(limit); + + const pubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); + + if (followList.length > 0) { + query = query.where('pubkey', 'in', followList); + } + + const followingPubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); + + return Array.from(followingPubkeys.union(pubkeys)); } From 8cd212e407ee9d11dda896ddd0c14182771fcee3 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 17 Sep 2024 11:09:39 -0300 Subject: [PATCH 3/8] test: add missing argument in getPubkeysBySearch() function --- src/utils/search.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/search.test.ts b/src/utils/search.test.ts index a67c0662..4fdb9d80 100644 --- a/src/utils/search.test.ts +++ b/src/utils/search.test.ts @@ -11,11 +11,11 @@ Deno.test('fuzzy search works', async () => { search: 'patrickReiis patrickdosreis.com', }).execute(); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1 }), []); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1 }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followList: [] }), []); + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followList: [] }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1 }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followList: [] }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); }); From f73b20bf0353a58dea65fc46deccd848736e7c9a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 17 Sep 2024 14:50:33 -0300 Subject: [PATCH 4/8] refactor: make getPubkeysBySearch() function use set of strings Set --- src/controllers/api/accounts.ts | 5 +---- src/controllers/api/search.ts | 5 +---- src/utils/search.test.ts | 6 +++--- src/utils/search.ts | 6 +++--- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 088d942b..0ca2f714 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -136,10 +136,7 @@ const accountSearchController: AppController = async (c) => { return c.json(pubkey ? [await accountFromPubkey(pubkey)] : []); } - const followList: string[] = []; - if (viewerPubkey) { - followList.push(...await getFollowedPubkeys(viewerPubkey)); - } + const followList: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); const pubkeys = (await getPubkeysBySearch(kysely, { q: query, limit, followList })).slice(0, limit); let events = event ? [event] : await store.query([{ kinds: [0], authors: pubkeys, limit }], { diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index ebcbc4ca..358a8882 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -96,10 +96,7 @@ async function searchEvents( if (type === 'accounts') { const kysely = await Storages.kysely(); - const followList: string[] = []; - if (viewerPubkey) { - followList.push(...await getFollowedPubkeys(viewerPubkey)); - } + const followList: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit, followList }))); if (!filter?.authors) { diff --git a/src/utils/search.test.ts b/src/utils/search.test.ts index 4fdb9d80..4b9b1d30 100644 --- a/src/utils/search.test.ts +++ b/src/utils/search.test.ts @@ -11,11 +11,11 @@ Deno.test('fuzzy search works', async () => { search: 'patrickReiis patrickdosreis.com', }).execute(); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followList: [] }), []); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followList: [] }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followList: new Set() }), []); + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followList: new Set() }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followList: [] }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followList: new Set() }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); }); diff --git a/src/utils/search.ts b/src/utils/search.ts index 81b9240a..a12e8c95 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -5,7 +5,7 @@ import { DittoTables } from '@/db/DittoTables.ts'; /** Get pubkeys whose name and NIP-05 is similar to 'q' */ export async function getPubkeysBySearch( kysely: Kysely, - opts: { q: string; limit: number; followList: string[] }, + opts: { q: string; limit: number; followList: Set }, ) { const { q, limit, followList } = opts; @@ -22,8 +22,8 @@ export async function getPubkeysBySearch( const pubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); - if (followList.length > 0) { - query = query.where('pubkey', 'in', followList); + if (followList.size > 0) { + query = query.where('pubkey', 'in', [...followList]); } const followingPubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); From f1c0d8c18fafe5baa18ff65c4befc116db792a42 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 18 Sep 2024 11:26:30 -0300 Subject: [PATCH 5/8] refactor(getPubkeysBySearch): rename followList to followedPubkeys --- src/controllers/api/accounts.ts | 4 ++-- src/controllers/api/search.ts | 4 ++-- src/utils/search.test.ts | 6 +++--- src/utils/search.ts | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 0ca2f714..f16f04e5 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -136,8 +136,8 @@ const accountSearchController: AppController = async (c) => { return c.json(pubkey ? [await accountFromPubkey(pubkey)] : []); } - const followList: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); - const pubkeys = (await getPubkeysBySearch(kysely, { q: query, limit, followList })).slice(0, limit); + const followedPubkeys: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); + const pubkeys = (await getPubkeysBySearch(kysely, { q: query, limit, followedPubkeys })).slice(0, limit); let events = event ? [event] : await store.query([{ kinds: [0], authors: pubkeys, limit }], { signal, diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 358a8882..01977208 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -96,8 +96,8 @@ async function searchEvents( if (type === 'accounts') { const kysely = await Storages.kysely(); - const followList: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); - pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit, followList }))); + const followedPubkeys: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); + pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit, followedPubkeys }))); if (!filter?.authors) { filter.authors = pubkeys; diff --git a/src/utils/search.test.ts b/src/utils/search.test.ts index 4b9b1d30..0621fdaa 100644 --- a/src/utils/search.test.ts +++ b/src/utils/search.test.ts @@ -11,11 +11,11 @@ Deno.test('fuzzy search works', async () => { search: 'patrickReiis patrickdosreis.com', }).execute(); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followList: new Set() }), []); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followList: new Set() }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followedPubkeys: new Set() }), []); + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followedPubkeys: new Set() }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followList: new Set() }), [ + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followedPubkeys: new Set() }), [ '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', ]); }); diff --git a/src/utils/search.ts b/src/utils/search.ts index a12e8c95..40d2676e 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -5,9 +5,9 @@ import { DittoTables } from '@/db/DittoTables.ts'; /** Get pubkeys whose name and NIP-05 is similar to 'q' */ export async function getPubkeysBySearch( kysely: Kysely, - opts: { q: string; limit: number; followList: Set }, + opts: { q: string; limit: number; followedPubkeys: Set }, ) { - const { q, limit, followList } = opts; + const { q, limit, followedPubkeys } = opts; let query = kysely .selectFrom('author_search') @@ -22,8 +22,8 @@ export async function getPubkeysBySearch( const pubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); - if (followList.size > 0) { - query = query.where('pubkey', 'in', [...followList]); + if (followedPubkeys.size > 0) { + query = query.where('pubkey', 'in', [...followedPubkeys]); } const followingPubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); From 4ae17c4993a2c73af9c6e2c441c9dda71de8677d Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 18 Sep 2024 14:15:18 -0300 Subject: [PATCH 6/8] refactor: make getPubkeysBySearch() function return Set --- src/controllers/api/accounts.ts | 2 +- src/controllers/api/search.ts | 8 ++++---- src/utils/search.test.ts | 20 +++++++++++++------- src/utils/search.ts | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index f16f04e5..f27b20e1 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -137,7 +137,7 @@ const accountSearchController: AppController = async (c) => { } const followedPubkeys: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); - const pubkeys = (await getPubkeysBySearch(kysely, { q: query, limit, followedPubkeys })).slice(0, limit); + const pubkeys = Array.from(await getPubkeysBySearch(kysely, { q: query, limit, followedPubkeys })); let events = event ? [event] : await store.query([{ kinds: [0], authors: pubkeys, limit }], { signal, diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 01977208..78b8990b 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -92,15 +92,15 @@ async function searchEvents( filter.authors = [account_id]; } - const pubkeys: string[] = []; + let pubkeys: Set = new Set(); if (type === 'accounts') { const kysely = await Storages.kysely(); const followedPubkeys: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); - pubkeys.push(...(await getPubkeysBySearch(kysely, { q, limit, followedPubkeys }))); + pubkeys = pubkeys.union(await getPubkeysBySearch(kysely, { q, limit, followedPubkeys })); if (!filter?.authors) { - filter.authors = pubkeys; + filter.authors = Array.from(pubkeys); } else { filter.authors.push(...pubkeys); } @@ -115,7 +115,7 @@ async function searchEvents( if (type !== 'accounts') return events; - events = pubkeys + events = Array.from(pubkeys) .map((pubkey) => events.find((event) => event.pubkey === pubkey)) .filter((event) => !!event); diff --git a/src/utils/search.test.ts b/src/utils/search.test.ts index 0621fdaa..1f01a157 100644 --- a/src/utils/search.test.ts +++ b/src/utils/search.test.ts @@ -11,11 +11,17 @@ Deno.test('fuzzy search works', async () => { search: 'patrickReiis patrickdosreis.com', }).execute(); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followedPubkeys: new Set() }), []); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followedPubkeys: new Set() }), [ - '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', - ]); - assertEquals(await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followedPubkeys: new Set() }), [ - '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', - ]); + assertEquals(await getPubkeysBySearch(db.kysely, { q: 'pat rick', limit: 1, followedPubkeys: new Set() }), new Set()); + assertEquals( + await getPubkeysBySearch(db.kysely, { q: 'patrick dos reis', limit: 1, followedPubkeys: new Set() }), + new Set([ + '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', + ]), + ); + assertEquals( + await getPubkeysBySearch(db.kysely, { q: 'dosreis.com', limit: 1, followedPubkeys: new Set() }), + new Set([ + '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4', + ]), + ); }); diff --git a/src/utils/search.ts b/src/utils/search.ts index 40d2676e..b0be761b 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -6,7 +6,7 @@ import { DittoTables } from '@/db/DittoTables.ts'; export async function getPubkeysBySearch( kysely: Kysely, opts: { q: string; limit: number; followedPubkeys: Set }, -) { +): Promise> { const { q, limit, followedPubkeys } = opts; let query = kysely @@ -28,5 +28,5 @@ export async function getPubkeysBySearch( const followingPubkeys = new Set((await query.execute()).map(({ pubkey }) => pubkey)); - return Array.from(followingPubkeys.union(pubkeys)); + return new Set(Array.from(followingPubkeys.union(pubkeys)).slice(0, limit)); } From 8890f6bce5247b51faf1111a7cb713e2f948d741 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 18 Sep 2024 12:58:17 -0500 Subject: [PATCH 7/8] searchEvents: fix account_id, simplify code --- src/controllers/api/search.ts | 52 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 78b8990b..ce7ca9f3 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -80,7 +80,12 @@ async function searchEvents( { q, type, limit, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string }, signal: AbortSignal, ): Promise { - if (type === 'hashtags') return Promise.resolve([]); + // Hashtag search is not supported. + if (type === 'hashtags') { + return Promise.resolve([]); + } + + const store = await Storages.search(); const filter: NostrFilter = { kinds: typeToKinds(type), @@ -88,36 +93,33 @@ async function searchEvents( limit, }; + // For account search, use a special index, and prioritize followed accounts. + if (type === 'accounts') { + const kysely = await Storages.kysely(); + + const followedPubkeys = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); + const searchPubkeys = await getPubkeysBySearch(kysely, { q, limit, followedPubkeys }); + + filter.authors = [...searchPubkeys]; + filter.search = undefined; + } + + // Results should only be shown from one author. if (account_id) { filter.authors = [account_id]; } - let pubkeys: Set = new Set(); - if (type === 'accounts') { - const kysely = await Storages.kysely(); - - const followedPubkeys: Set = viewerPubkey ? await getFollowedPubkeys(viewerPubkey) : new Set(); - pubkeys = pubkeys.union(await getPubkeysBySearch(kysely, { q, limit, followedPubkeys })); - - if (!filter?.authors) { - filter.authors = Array.from(pubkeys); - } else { - filter.authors.push(...pubkeys); - } - - filter.search = undefined; - } - - const store = await Storages.search(); - - let events = await store.query([filter], { signal }) + // Query the events. + let events = await store + .query([filter], { signal }) .then((events) => hydrateEvents({ events, store, signal })); - if (type !== 'accounts') return events; - - events = Array.from(pubkeys) - .map((pubkey) => events.find((event) => event.pubkey === pubkey)) - .filter((event) => !!event); + // When using an authors filter, return the events in the same order as the filter. + if (filter.authors) { + events = filter.authors + .map((pubkey) => events.find((event) => event.pubkey === pubkey)) + .filter((event) => !!event); + } return events; } From 5ecf016cb98dd898ed9ce4bc1a55585712670eae Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 18 Sep 2024 13:42:43 -0500 Subject: [PATCH 8/8] EventsDB: fix domain search performance (also allow searching by multiple languages/domains) --- src/storages/EventsDB.ts | 53 ++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index b4dc0b9b..7d61c2de 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -153,22 +153,16 @@ class EventsDB extends NPostgres { search: tokens.filter((t) => typeof t === 'string').join(' '), }) as SelectQueryBuilder>; - const data = tokens.filter((t) => typeof t === 'object').reduce( - (acc, t) => acc.set(t.key, t.value), - new Map(), - ); + const languages = new Set(); - const domain = data.get('domain'); - const language = data.get('language'); - - if (domain) { - query = query - .innerJoin('pubkey_domains', 'nostr_events.pubkey', 'pubkey_domains.pubkey') - .where('pubkey_domains.domain', '=', domain); + for (const token of tokens) { + if (typeof token === 'object' && token.key === 'language') { + languages.add(token.value); + } } - if (language) { - query = query.where('language', '=', language); + if (languages.size) { + query = query.where('language', 'in', [...languages]); } return query; @@ -288,6 +282,39 @@ class EventsDB extends NPostgres { filters = structuredClone(filters); for (const filter of filters) { + if (filter.search) { + const tokens = NIP50.parseInput(filter.search); + + const domains = new Set(); + + for (const token of tokens) { + if (typeof token === 'object' && token.key === 'domain') { + domains.add(token.value); + } + } + + if (domains.size) { + const query = this.opts.kysely + .selectFrom('pubkey_domains') + .select('pubkey') + .where('domain', 'in', [...domains]); + + if (filter.authors) { + query.where('pubkey', 'in', filter.authors); + } + + const pubkeys = await query.execute().then((rows) => rows.map((row) => row.pubkey)); + + filter.authors = pubkeys; + } + + // Re-serialize the search string without the domain key. :facepalm: + filter.search = tokens + .filter((t) => typeof t === 'object' && t.key !== 'domain') + .map((t) => typeof t === 'object' ? `${t.key}:${t.value}` : t) + .join(' '); + } + if (filter.kinds) { // Ephemeral events are not stored, so don't bother querying for them. // If this results in an empty kinds array, NDatabase will remove the filter before querying and return no results.