Class: Publication::TextSearch
- Inherits:
-
Object
- Object
- Publication::TextSearch
- Defined in:
- app/services/publication/text_search.rb
Overview
Discovery half of cross-publication find-and-replace ("find every PDF whose
spec says X"): searches the docling-extracted text cache
(+uploads.extracted_markdown+) of every publication's literature for a
literal phrase and reports the blast radius — a COUNT and a sample — before
anything is enqueued (the bulk-op rule: count first, run second).
Notes:
- Matches the docling extraction, NOT the raw PDF text layer. A publication
whose literature was never extracted won't be found (run
UploadMarkdownExtractionWorker / the backfill script first). - The edit half is Pdf::TextReplacer, driven per-document by
PublicationTextReplaceWorker; Sunny's +pdf_find_replace+ tool ties them
together with a dry-run → confirm flow.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- SAMPLE_SIZE =
How many matching publications are sampled into Result#samples.
10
Class Method Summary collapse
Class Method Details
.call(query, scope: Item.publications) ⇒ Result
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'app/services/publication/text_search.rb', line 33 def self.call(query, scope: Item.publications) raise ArgumentError, 'query must be present' if query.to_s.strip.empty? # Substring search on uploads.extracted_markdown is intentionally a # sequential scan: a leading-wildcard ILIKE can't use a b-tree index (and # no trigram index exists on purpose). That's acceptable here — the scan # is bounded by the relation's other filters (e.g. the active-only scope # the tool passes), the result set is capped downstream (SAMPLE_SIZE here, # FIND_REPLACE_MAX_ENQUEUE at enqueue time), and this is a dry-run # blast-radius check, not a hot path. relation = matching_relation(query, scope) Result.new( count: relation.count, samples: relation.limit(SAMPLE_SIZE).pluck(:id, :sku, :name).map { |id, sku, name| { id:, sku:, name: } }, relation: ) end |