search: fix FTS special characters, optimize search

This commit is contained in:
Alex Gleason 2023-08-30 14:03:16 -05:00
parent df14ff66bc
commit 675010ddec
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
4 changed files with 58 additions and 14 deletions

View File

@ -1,27 +1,70 @@
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import * as eventsDB from '@/db/events.ts'; import * as eventsDB from '@/db/events.ts';
import { lookupAccount } from '@/utils.ts'; import { type Event, nip05, nip19 } from '@/deps.ts';
import * as mixer from '@/mixer.ts';
import { lookupNip05Cached } from '@/nip05.ts';
import { getAuthor } from '@/queries.ts';
import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
import { bech32ToPubkey, dedupeEvents, Time } from '@/utils.ts';
import { paginationSchema } from '@/utils/web.ts';
const searchController: AppController = async (c) => { const searchController: AppController = async (c) => {
const q = c.req.query('q'); const q = c.req.query('q');
const params = paginationSchema.parse(c.req.query());
if (!q) { if (!q) {
return c.json({ error: 'Missing `q` query parameter.' }, 422); return c.json({ error: 'Missing `q` query parameter.' }, 422);
} }
// For now, only support looking up accounts. const [event, events] = await Promise.all([
// TODO: Support searching statuses and hashtags. lookupEvent(decodeURIComponent(q)),
const event = await lookupAccount(decodeURIComponent(q)); eventsDB.getFilters<number>([{ kinds: [1], search: q, ...params }]),
]);
const events = await eventsDB.getFilters([{ kinds: [1], search: q }]); if (event) {
const statuses = await Promise.all(events.map((event) => toStatus(event, c.get('pubkey')))); events.push(event);
}
const results = dedupeEvents(events);
const accounts = await Promise.all(
results
.filter((event): event is Event<0> => event.kind === 0)
.map((event) => toAccount(event)),
);
const statuses = await Promise.all(
results
.filter((event): event is Event<1> => event.kind === 1)
.map((event) => toStatus(event, c.get('pubkey'))),
);
return c.json({ return c.json({
accounts: event ? [await toAccount(event)] : [], accounts: accounts.filter(Boolean),
statuses: statuses.filter(Boolean), statuses: statuses.filter(Boolean),
hashtags: [], hashtags: [],
}); });
}; };
/** Resolve a searched value into an event, if applicable. */
async function lookupEvent(value: string): Promise<Event<0 | 1> | undefined> {
if (new RegExp(`^${nip19.BECH32_REGEX.source}$`).test(value)) {
const pubkey = bech32ToPubkey(value);
if (pubkey) {
return getAuthor(pubkey);
}
} else if (/^[0-9a-f]{64}$/.test(value)) {
const [event] = await mixer.getFilters(
[{ kinds: [0], authors: [value], limit: 1 }, { kinds: [1], ids: [value], limit: 1 }],
{ limit: 1, timeout: Time.seconds(1) },
);
return event;
} else if (nip05.NIP05_REGEX.test(value)) {
const pubkey = await lookupNip05Cached(value);
if (pubkey) {
return getAuthor(pubkey);
}
}
}
export { searchController }; export { searchController };

View File

@ -120,7 +120,7 @@ function getFilterQuery(filter: DittoFilter) {
if (filter.search) { if (filter.search) {
query = query query = query
.innerJoin('events_fts', 'events_fts.id', 'events.id') .innerJoin('events_fts', 'events_fts.id', 'events.id')
.where('events_fts.content', 'match', filter.search); .where('events_fts.content', 'match', JSON.stringify(filter.search));
} }
return query; return query;

View File

@ -2,7 +2,7 @@ import { type Event, matchFilters } from '@/deps.ts';
import * as client from '@/client.ts'; import * as client from '@/client.ts';
import * as eventsDB from '@/db/events.ts'; import * as eventsDB from '@/db/events.ts';
import { eventDateComparator } from '@/utils.ts'; import { dedupeEvents, eventDateComparator } from '@/utils.ts';
import type { DittoFilter, GetFiltersOpts } from '@/filter.ts'; import type { DittoFilter, GetFiltersOpts } from '@/filter.ts';
@ -33,11 +33,6 @@ function unmixEvents<K extends number>(events: Event<K>[], filters: DittoFilter<
return events; return events;
} }
/** Deduplicate events by ID. */
function dedupeEvents<K extends number>(events: Event<K>[]): Event<K>[] {
return [...new Map(events.map((event) => [event.id, event])).values()];
}
/** Take the newest events among replaceable ones. */ /** Take the newest events among replaceable ones. */
function takeNewestEvents<K extends number>(events: Event<K>[]): Event<K>[] { function takeNewestEvents<K extends number>(events: Event<K>[]): Event<K>[] {
const isReplaceable = (kind: number) => const isReplaceable = (kind: number) =>

View File

@ -101,8 +101,14 @@ function isFollowing(source: Event<3>, targetPubkey: string): boolean {
); );
} }
/** Deduplicate events by ID. */
function dedupeEvents<K extends number>(events: Event<K>[]): Event<K>[] {
return [...new Map(events.map((event) => [event.id, event])).values()];
}
export { export {
bech32ToPubkey, bech32ToPubkey,
dedupeEvents,
eventAge, eventAge,
eventDateComparator, eventDateComparator,
findTag, findTag,