Poster search through a posters table and releases_poster_newest_idx; complete releases are final; the tail before everything (-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9a9c0155e
commit
c0e00f1707
8 files changed
+105
-8
No files matched your search
@@ -243,6 +243,9 @@ class BatchStore {
|
||||
taken.set(r.key, n)
|
||||
return { release_key: n === 1 ? r.key : `${r.key}:${n}`, key: r.key, upload: r.upload, ...row(r) }
|
||||
}
|
||||
// Their posters, for poster search (posters: one row per poster, searched by trigram; migration 4000).
|
||||
const posters = [...new Set(fresh.map((r) => r.poster).filter((p): p is string => !!p))]
|
||||
if (posters.length > 0) await trx.rawQuery(`INSERT INTO posters (poster) SELECT DISTINCT left(p, 200) FROM unnest(?::text[]) AS p ON CONFLICT DO NOTHING`, [posters])
|
||||
let inserts = fresh.map(numbered)
|
||||
// Writers of other groups number the same key at the same time (a spam post crossposted to 67
|
||||
// groups; only the group is locked): a number taken meanwhile is skipped, and the next one tried.
|
||||
|
||||
@@ -78,7 +78,7 @@ export const RULES: Rule[] = [
|
||||
// Final (release_final): next to an unread range its par2 or the rest of its files may still come (LOG.md 2026-10-16 (67)).
|
||||
sql: `SELECT r.id FROM releases r
|
||||
WHERE r.settled_at < now() - interval '1 day' AND NOT r.readable AND r.inner_name IS NULL
|
||||
AND release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at)
|
||||
AND release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at, r.complete)
|
||||
AND NOT EXISTS (SELECT 1 FROM release_merges m WHERE m.into_id = r.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM release_spots s WHERE s.release_id = r.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM par2_sets p WHERE p.release_id = r.id)`,
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface ReleaseSummary {
|
||||
innerName: string | null
|
||||
}
|
||||
|
||||
/** Posters a poster search looks in at most (a short fragment matches thousands of random ones). */
|
||||
const MAX_POSTERS = 1000
|
||||
const SORTS: Record<SortOrder, string> = {
|
||||
newest: 'r.posted_at DESC NULLS LAST, r.id DESC',
|
||||
oldest: 'r.posted_at ASC NULLS LAST, r.id ASC',
|
||||
@@ -95,14 +97,17 @@ class SearchService {
|
||||
if (params.readable) where.push('(r.readable OR r.inner_name IS NOT NULL)')
|
||||
if (params.complete) where.push('r.complete')
|
||||
// Incomplete only when final: a release next to unread ranges may yet get its missing files (release_final).
|
||||
if (params.notIncomplete) where.push('(r.complete IS NOT FALSE OR NOT release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at))')
|
||||
if (params.notIncomplete) where.push('(r.complete IS NOT FALSE OR NOT release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at, r.complete))')
|
||||
if (params.unlocked) where.push('r.passworded IS NOT TRUE')
|
||||
if (params.source) {
|
||||
where.push('r.source = ?')
|
||||
bindings.push(params.source)
|
||||
}
|
||||
if (params.poster) {
|
||||
where.push('r.poster ILIKE ?')
|
||||
// The matching posters first (posters, by trigram), then their releases by releases_poster_newest_idx: "poster ILIKE"
|
||||
// on releases walked all 50M rows newest first, 30+ min for a poster with few releases (LOG.md 2026-10-16 (70)).
|
||||
// By the first 200 characters, as posters and the index keep them (a btree entry holds at most 2,704 bytes).
|
||||
where.push(`left(r.poster, 200) = ANY(ARRAY(SELECT p.poster FROM posters p WHERE p.poster ILIKE ? LIMIT ${MAX_POSTERS}))`)
|
||||
bindings.push(contains(params.poster))
|
||||
}
|
||||
if (params.categories?.length) {
|
||||
@@ -137,7 +142,7 @@ class SearchService {
|
||||
const [{ rows }, { rows: counted }] = await Promise.all([
|
||||
db.rawQuery(
|
||||
`SELECT r.id, r.name, r.source, r.poster, r.groups, r.posted_at, r.file_total, r.files, r.bytes, r.readable, r.complete, r.category, r.settled_at, r.passworded, r.inner_name,
|
||||
release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at) AS final
|
||||
release_final(r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at, r.complete) AS final
|
||||
FROM releases r WHERE ${whereSql} ORDER BY ${order} LIMIT ? OFFSET ?`,
|
||||
[...bindings, perPage, offset]
|
||||
),
|
||||
@@ -178,7 +183,7 @@ class SearchService {
|
||||
const [files, { rows: inspections }, { rows: finality }] = await Promise.all([
|
||||
NzbService.files(id),
|
||||
db.rawQuery(`SELECT kind, result, created_at FROM release_inspections WHERE release_id = ? ORDER BY candidate_id`, [id]),
|
||||
db.rawQuery(`SELECT release_final(?, ?, ?, ?, ?) AS final`, [r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at]),
|
||||
db.rawQuery(`SELECT release_final(?, ?, ?, ?, ?, ?) AS final`, [r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at, r.complete]),
|
||||
])
|
||||
return {
|
||||
id: Number(r.id),
|
||||
|
||||
Reference in new issue
Block a user