diff --git a/deno.json b/deno.json index dabb1ac9..18a6621c 100644 --- a/deno.json +++ b/deno.json @@ -48,7 +48,7 @@ "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@negrel/webpush": "jsr:@negrel/webpush@^0.3.0", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/db": "jsr:@nostrify/db@^0.38.0", + "@nostrify/db": "jsr:@nostrify/db@^0.39.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.38.1", "@nostrify/policies": "jsr:@nostrify/policies@^0.36.1", "@nostrify/types": "jsr:@nostrify/types@^0.36.0", diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 8bfe4ffd..c0a4a54e 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -11,7 +11,7 @@ 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 { getIdsBySearch, getPubkeysBySearch } from '@/utils/search.ts'; +import { getPubkeysBySearch } from '@/utils/search.ts'; const searchQuerySchema = z.object({ q: z.string().transform(decodeURIComponent), @@ -105,13 +105,6 @@ async function searchEvents( filter.search = undefined; } - // For status search, use a specific query so it supports offset and is open to customizations. - if (type === 'statuses') { - const ids = await getIdsBySearch(kysely, { q, limit, offset }); - filter.ids = [...ids]; - filter.search = undefined; - } - // Results should only be shown from one author. if (account_id) { filter.authors = [account_id]; diff --git a/src/utils/search.test.ts b/src/utils/search.test.ts index 71f96de2..056c2927 100644 --- a/src/utils/search.test.ts +++ b/src/utils/search.test.ts @@ -1,7 +1,7 @@ import { assertEquals } from '@std/assert'; -import { createTestDB, genEvent } from '@/test.ts'; -import { getIdsBySearch, getPubkeysBySearch } from '@/utils/search.ts'; +import { createTestDB } from '@/test.ts'; +import { getPubkeysBySearch } from '@/utils/search.ts'; Deno.test('fuzzy search works', async () => { await using db = await createTestDB(); @@ -48,47 +48,3 @@ Deno.test('fuzzy search works with offset', async () => { new Set(), ); }); - -Deno.test('Searching for posts work', async () => { - await using db = await createTestDB(); - - const event = genEvent({ content: "I'm not an orphan. Death is my importance", kind: 1 }); - await db.store.event(event); - await db.kysely.updateTable('nostr_events').set('search_ext', { language: 'en' }).where('id', '=', event.id) - .execute(); - - const event2 = genEvent({ content: 'The more I explore is the more I fall in love with the music I make.', kind: 1 }); - await db.store.event(event2); - await db.kysely.updateTable('nostr_events').set('search_ext', { language: 'en' }).where('id', '=', event2.id) - .execute(); - - assertEquals( - await getIdsBySearch(db.kysely, { q: 'Death is my importance', limit: 1, offset: 0 }), // ordered words - new Set([event.id]), - ); - - assertEquals( - await getIdsBySearch(db.kysely, { q: 'make I music', limit: 1, offset: 0 }), // reversed words - new Set([event2.id]), - ); - - assertEquals( - await getIdsBySearch(db.kysely, { q: 'language:en make I music', limit: 10, offset: 0 }), // reversed words, english - new Set([event2.id]), - ); - - assertEquals( - await getIdsBySearch(db.kysely, { q: 'language:en an orphan', limit: 10, offset: 0 }), // all posts in english plus search - new Set([event.id]), - ); - - assertEquals( - await getIdsBySearch(db.kysely, { q: 'language:en', limit: 10, offset: 0 }), // all posts in english - new Set([event.id, event2.id]), - ); - - assertEquals( - await getIdsBySearch(db.kysely, { q: '', limit: 10, offset: 0 }), - new Set(), - ); -}); diff --git a/src/utils/search.ts b/src/utils/search.ts index f44e00c8..29ecefd9 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -1,7 +1,6 @@ import { Kysely, sql } from 'kysely'; import { DittoTables } from '@/db/DittoTables.ts'; -import { NIP50 } from '@nostrify/nostrify'; /** Get pubkeys whose name and NIP-05 is similar to 'q' */ export async function getPubkeysBySearch( @@ -33,109 +32,3 @@ export async function getPubkeysBySearch( return new Set(Array.from(followingPubkeys.union(pubkeys))); } - -/** - * Get kind 1 ids whose content matches `q`. - * It supports NIP-50 extensions. - */ -export async function getIdsBySearch( - kysely: Kysely, - opts: { q: string; limit: number; offset: number }, -): Promise> { - const { q, limit, offset } = opts; - - const [lexemes] = (await sql<{ phraseto_tsquery: 'string' }>`SELECT phraseto_tsquery(${q})`.execute(kysely)).rows; - - // if it's just stop words, don't bother making a request to the database - if (!lexemes.phraseto_tsquery) { - return new Set(); - } - - const tokens = NIP50.parseInput(q); - - const ext: Record = {}; - const txt = tokens.filter((token) => typeof token === 'string').join(' '); - - let query = kysely - .selectFrom('nostr_events') - .select('id') - .where('kind', '=', 1) - .orderBy(['created_at desc']) - .limit(limit) - .offset(offset); - - const domains = new Set(); - - for (const token of tokens) { - if (typeof token === 'object' && token.key === 'domain') { - domains.add(token.value); - } - } - - for (const token of tokens) { - if (typeof token === 'object') { - ext[token.key] ??= []; - ext[token.key].push(token.value); - } - } - - for (let [key, values] of Object.entries(ext)) { - if (key === 'domain' || key === '-domain') continue; - - let negated = false; - - if (key.startsWith('-')) { - key = key.slice(1); - negated = true; - } - - query = query.where((eb) => { - if (negated) { - return eb.and( - values.map((value) => eb.not(eb('nostr_events.search_ext', '@>', { [key]: value }))), - ); - } else { - return eb.or( - values.map((value) => eb('nostr_events.search_ext', '@>', { [key]: value })), - ); - } - }); - } - - if (domains.size) { - const pubkeys = (await kysely - .selectFrom('pubkey_domains') - .select('pubkey') - .where('domain', 'in', [...domains]) - .execute()).map(({ pubkey }) => pubkey); - - query = query.where('pubkey', 'in', pubkeys); - } - - // If there is not a specific content to search, return the query already - // This is useful if the person only makes a query search such as `domain:patrickdosreis.com` - if (!txt.length) { - const ids = new Set((await query.execute()).map(({ id }) => id)); - return ids; - } - - let fallbackQuery = query; - if (txt) { - query = query.where('search', '@@', sql`phraseto_tsquery(${txt})`); - } - - const ids = new Set((await query.execute()).map(({ id }) => id)); - - // If there is no ids, fallback to `plainto_tsquery` - if (!ids.size) { - fallbackQuery = fallbackQuery.where( - 'search', - '@@', - sql`plainto_tsquery(${txt})`, - ); - const ids = new Set((await fallbackQuery.execute()).map(({ id }) => id)); - return ids; - } - - return ids; -}