feat(search): index PDF text, OCR scanned PDFs on import
All checks were successful
CI / Windows build (push) Successful in 15m50s

Search now covers handwriting, the PDF text layer, AND scanned
(rasterized) PDFs.

- PdfTextIndexer runs at import: sums the embedded text layer across
  pages; if present it stores that as the document body, otherwise the
  PDF is rasterized and its rendered pages are OCR'd in the background.
  The result lands in the sidecar `pageText` field (distinct from
  `ocrText`, the handwriting OCR). Idempotent (skips a sidecar that
  already has pageText); degrades gracefully with no OCR engine.
- pdfrx_page_text_source abstracts text/render so it's testable.
- VaultSearchIndex now harvests title + typed text + handwriting OCR +
  PDF pageText, so search finds notes, typed PDFs and scanned PDFs.

analyze clean, 409 tests green.
This commit is contained in:
2026-06-25 00:23:19 +08:00
parent 20add27a30
commit e939759458
10 changed files with 668 additions and 16 deletions

View File

@@ -313,6 +313,7 @@ class BadnoteSidecar {
List<SidecarScratchLink>? scratchLinks,
Map<int, String>? legacyAnnotations,
this.ocrText,
this.pageText,
this.legacyId,
this.background,
}) : strokes = strokes ?? <int, List<EditorStroke>>{},
@@ -368,6 +369,17 @@ class BadnoteSidecar {
/// the notebook has no handwriting or OCR hasn't run.
final String? ocrText;
/// Searchable text of the underlying DOCUMENT BODY for a file-backed notebook
/// (a PDF), captured ONCE at import time so the vault-scan search index covers
/// the document — not just the user's annotations. It is either the PDF's
/// embedded (printed) text layer, or — for a RASTERIZED / scanned PDF with no
/// text layer — the result of a background OCR pass over the rendered pages.
/// Pages are joined with `\f` (form feed) but the index treats it as a flat
/// blob. Null when the document has not been indexed yet (back-compat: an old
/// sidecar simply omits the field) or has no extractable/recognized text. This
/// is distinct from [ocrText], which holds ONLY handwriting OCR.
final String? pageText;
/// The legacy SQLite row id this sidecar was migrated from (a `documents.id`
/// or `notes.id`). Set ONLY by the one-time migration; it makes the migration
/// idempotent (a re-run recognizes an already-migrated item by this id even if
@@ -413,6 +425,7 @@ class BadnoteSidecar {
entry.key.toString(): entry.value,
},
if (ocrText != null && ocrText!.isNotEmpty) 'ocrText': ocrText,
if (pageText != null && pageText!.isNotEmpty) 'pageText': pageText,
if (legacyId != null) 'legacyId': legacyId,
if (background != null) 'background': background,
};
@@ -470,6 +483,7 @@ class BadnoteSidecar {
return out;
}(),
ocrText: json['ocrText'] as String?,
pageText: json['pageText'] as String?,
legacyId: json['legacyId'] as String?,
background: json['background'] as String?,
);