F6 — PDF-контент в поиске (сейчас не индексируется) #42

Closed
opened 2026-09-16 18:14:05 +01:00 by eSlider · 3 comments
Owner

Epic: #34

F6 — PDF-контент в поиске (сейчас не индексируется)

Проблема

OnlyOffice ES индексирует содержимое только office-форматов (docx/xlsx/pptx).
PDF попадает в индекс по имени (см. F3 #37, docs/elasticsearch.md). Поэтому
oo search --content "S1019" не находит номера внутри PDF-счетов.

Research (обязательно, до кода)

Разобрать в исходниках CommunityServer (web/.../Products/Files/Core/Search/FilesWrapper.cs,
module/ASC.ElasticSearch/Core/WrapperWithDoc.cs, FileUtility.CanIndex) — почему
PDF исключён и какие есть пути:

  1. Включить PDF в CanIndex + reindex (патч сервера — рискованно, ломается
    при обновлении OO).
  2. Отдельный ingest-пайплайн/attachment-процессор для PDF в ES (server-side).
  3. Свой индекс в ES, наполняемый нашим конвейером (internal/docpipe/OCR),
    не трогая индекс OnlyOffice — отдельный индекс/алиас, esSearcher умеет и его.
  4. Не индексировать, а искать по PDF локально (docpipe + pdftotext) — fallback.

Оценить риски/сложность/детерминированность. Зафиксировать решение в
docs/elasticsearch.md.

Реализация (по выбранному пути, детерминированно)

  • Если путь 3 (наш индекс): отдельный ES-индекс (напр. oo_docs_text),
    наполнение из docpipe, Searcher умеет его; oo search --content --backend own.
  • Если путь 4: oo search --content --files с локальным извлечением текста.
  • Никаких правок сервера OO без явного решения PO/владельца.

Критерии

  • Решение зафиксировано (с рисками) в docs/elasticsearch.md.
  • Если реализуемо безопасно — oo search --content "<текст из PDF>" находит
    PDF; иначе — блокер с точной причиной.
  • go build/vet/test зелёные; интеграционные — skip без сервиса.
Epic: #34 ## F6 — PDF-контент в поиске (сейчас не индексируется) ### Проблема OnlyOffice ES индексирует содержимое только office-форматов (docx/xlsx/pptx). PDF попадает в индекс по **имени** (см. F3 #37, `docs/elasticsearch.md`). Поэтому `oo search --content "S1019"` не находит номера внутри PDF-счетов. ### Research (обязательно, до кода) Разобрать в исходниках CommunityServer (`web/.../Products/Files/Core/Search/FilesWrapper.cs`, `module/ASC.ElasticSearch/Core/WrapperWithDoc.cs`, `FileUtility.CanIndex`) — почему PDF исключён и какие есть пути: 1. Включить PDF в `CanIndex` + reindex (патч сервера — **рискованно**, ломается при обновлении OO). 2. Отдельный ingest-пайплайн/attachment-процессор для PDF в ES (server-side). 3. **Свой индекс** в ES, наполняемый нашим конвейером (`internal/docpipe`/OCR), не трогая индекс OnlyOffice — отдельный индекс/алиас, `esSearcher` умеет и его. 4. Не индексировать, а искать по PDF локально (`docpipe` + `pdftotext`) — fallback. Оценить риски/сложность/детерминированность. Зафиксировать решение в `docs/elasticsearch.md`. ### Реализация (по выбранному пути, детерминированно) - Если путь 3 (наш индекс): отдельный ES-индекс (напр. `oo_docs_text`), наполнение из docpipe, `Searcher` умеет его; `oo search --content --backend own`. - Если путь 4: `oo search --content --files` с локальным извлечением текста. - Никаких правок сервера OO без явного решения PO/владельца. ### Критерии - [ ] Решение зафиксировано (с рисками) в `docs/elasticsearch.md`. - [ ] Если реализуемо безопасно — `oo search --content "<текст из PDF>"` находит PDF; иначе — блокер с точной причиной. - [ ] `go build/vet/test` зелёные; интеграционные — skip без сервиса.
eSlider added the research label 2026-09-16 18:14:05 +01:00
Author
Owner

F6 — отчёт: PDF-контент в поиске

PR: #44
Ветка feat/pdf-content#42 · коммит 7c3a0b8 (base main).
Не мержу и не закрываю.

Решение и почему

Research по исходникам CommunityServer:

  • web/core/ASC.Web.Core/Files/FileUtility.cs — CanIndex читает серверную
    настройку files.index.formats (default ".pptx|.xlsx|.docx").
  • Products/Files/Core/Search/FilesWrapper.cs — GetDocumentStream* возвращает
    null при !CanIndex/encrypted/большой файл.
  • ASC.ElasticSearch/Core/WrapperWithDoc.cs + маппинг — формат-агностичны.

Значит PDF исключён настройкой, а не кодом. Варианты: (a) правка
files.index.formats + reindex 39k — это сервер, настройка теряется при
обновлении, Tika не OCR (сканы пусты) → отклонён; (b) server-side ingest — то
же; (c) свой индекс — выбран; (d) локальный поиск — без индекса/highlight,
отклонён.

Итог: вариант c — отдельный индекс oo_docs_text, наполняемый нашим
internal/docpipe (pdftotext + OCR). Сервер OO и его индекс files_file не
меняются. Решение зафиксировано в docs/elasticsearch.md (таблица вариантов,
риски, маппинг, грабли).

Что сделано

  • file_es_text.go — ESTextIndex (Name()="es-text"): Ensure (явный
    маппинг), Put (bulk, refresh), Delete (по id), Search (multi_match
    title^2 + content, фильтры folder/ext, highlight). Только stdlib.
  • file_text_index.go — TextIndexer: FileStore.List/Stat/Download →
    docpipe-экстракция → TextIndex; пул воркеров, PlanFolder/PlanFiles.
  • file_es.go — добавлены terms-клауза и content в highlight.
  • CLI: oo index folder FOLDER_ID | files FILE_ID...
    (--recursive --exts --limit --workers --lang --dry-run);
    oo search --backend oo|own.
  • docs/elasticsearch.md, README.md, .env.example
    (ONLYOFFICE_ES_TEXT_INDEX, default oo_docs_text), docstring main.go.

Тесты

Unit (без сети):

  • TestESTextSearchRequestShape, TestESTextBulkBody, TestParseESTextResponse,
    TestNewESTextIndexDefaults, TestESTextConfigFromEnvIndexDefault,
    TestTextIndexerIndexEntries, TestTextIndexerRecordsExtractionFailure,
    TestTextIndexerPlanFolder — PASS.
  • cmd/oo: TestIndexCommandRegistered, TestIndexSearchBackendFlag,
    TestSplitList — PASS.

Integration (//go:build integration, skip без ONLYOFFICE_ES_URL):

  • TestIntegrationESTextIndex — create/put/search/filters/delete/drop — PASS live.
  • TestIntegrationESSearch (F3, регрессия) — PASS live.

go build ./..., go vet ./..., go test -race ./... — зелёные.
gitleaks — no leaks; po scan — clean. go vet -tags=integration ./ — чисто.

Live-проверка (свой индекс, папки 634/639, 22 PDF)

oo index folder 634 + oo index folder 639 (indexed 14+8, failed 0), затем
oo search "<N>" --content --backend own:

  • S1019 → id 3576 2026-07-28-112626-Edelweiss-rechnung.pdf
  • S1020 → id 3577 2026-07-28-114826-Edelweiss-rechnung.pdf
  • S1021 → id 3578 2026-07-28-S1021-Edelweiss-rechnung.pdf
  • S1030 → id 3581 2026-07-28-S1030-Edelweiss-rechnung.pdf
  • S1055 → id 3591 S1055.pdf + id 3584 2026-08-20-S1055-… (дубль, ожидаемо)
  • S1063 → пусто: такого счёта в проиндексированных папках нет.

Контроль: те же запросы с --backend oo (default) — пусто, т.е. PDF-контент
в индексе OnlyOffice действительно не ищется.

Блокеры

Нет. Ограничение: индекс наполняется вручную (oo index), после изменений PDF
надо повторить (идемпотентный upsert по id).

## F6 — отчёт: PDF-контент в поиске PR: https://git.produktor.io/eSlider/go-onlyoffice/pulls/44 Ветка `feat/pdf-content#42` · коммит `7c3a0b8` (base `main`). Не мержу и не закрываю. ### Решение и почему Research по исходникам CommunityServer: - `web/core/ASC.Web.Core/Files/FileUtility.cs` — `CanIndex` читает серверную настройку `files.index.formats` (default `".pptx|.xlsx|.docx"`). - `Products/Files/Core/Search/FilesWrapper.cs` — `GetDocumentStream*` возвращает `null` при `!CanIndex`/encrypted/большой файл. - `ASC.ElasticSearch/Core/WrapperWithDoc.cs` + маппинг — формат-агностичны. Значит PDF исключён **настройкой**, а не кодом. Варианты: (a) правка `files.index.formats` + reindex 39k — это сервер, настройка теряется при обновлении, Tika не OCR (сканы пусты) → отклонён; (b) server-side ingest — то же; (c) **свой индекс** — выбран; (d) локальный поиск — без индекса/highlight, отклонён. Итог: **вариант c** — отдельный индекс `oo_docs_text`, наполняемый нашим `internal/docpipe` (pdftotext + OCR). Сервер OO и его индекс `files_file` не меняются. Решение зафиксировано в `docs/elasticsearch.md` (таблица вариантов, риски, маппинг, грабли). ### Что сделано - `file_es_text.go` — `ESTextIndex` (`Name()="es-text"`): `Ensure` (явный маппинг), `Put` (bulk, refresh), `Delete` (по id), `Search` (`multi_match` title^2 + content, фильтры `folder`/`ext`, highlight). Только stdlib. - `file_text_index.go` — `TextIndexer`: `FileStore.List/Stat/Download` → docpipe-экстракция → `TextIndex`; пул воркеров, `PlanFolder/PlanFiles`. - `file_es.go` — добавлены `terms`-клауза и `content` в highlight. - CLI: `oo index folder FOLDER_ID | files FILE_ID...` (`--recursive --exts --limit --workers --lang --dry-run`); `oo search --backend oo|own`. - `docs/elasticsearch.md`, `README.md`, `.env.example` (`ONLYOFFICE_ES_TEXT_INDEX`, default `oo_docs_text`), docstring `main.go`. ### Тесты Unit (без сети): - `TestESTextSearchRequestShape`, `TestESTextBulkBody`, `TestParseESTextResponse`, `TestNewESTextIndexDefaults`, `TestESTextConfigFromEnvIndexDefault`, `TestTextIndexerIndexEntries`, `TestTextIndexerRecordsExtractionFailure`, `TestTextIndexerPlanFolder` — PASS. - `cmd/oo`: `TestIndexCommandRegistered`, `TestIndexSearchBackendFlag`, `TestSplitList` — PASS. Integration (`//go:build integration`, skip без `ONLYOFFICE_ES_URL`): - `TestIntegrationESTextIndex` — create/put/search/filters/delete/drop — PASS live. - `TestIntegrationESSearch` (F3, регрессия) — PASS live. `go build ./...`, `go vet ./...`, `go test -race ./...` — зелёные. gitleaks — no leaks; `po scan` — clean. `go vet -tags=integration ./` — чисто. ### Live-проверка (свой индекс, папки 634/639, 22 PDF) `oo index folder 634` + `oo index folder 639` (indexed 14+8, failed 0), затем `oo search "<N>" --content --backend own`: - S1019 → id 3576 `2026-07-28-112626-Edelweiss-rechnung.pdf` - S1020 → id 3577 `2026-07-28-114826-Edelweiss-rechnung.pdf` - S1021 → id 3578 `2026-07-28-S1021-Edelweiss-rechnung.pdf` - S1030 → id 3581 `2026-07-28-S1030-Edelweiss-rechnung.pdf` - S1055 → id 3591 `S1055.pdf` + id 3584 `2026-08-20-S1055-…` (дубль, ожидаемо) - S1063 → пусто: такого счёта в проиндексированных папках нет. Контроль: те же запросы с `--backend oo` (default) — пусто, т.е. PDF-контент в индексе OnlyOffice действительно не ищется. ### Блокеры Нет. Ограничение: индекс наполняется вручную (`oo index`), после изменений PDF надо повторить (идемпотентный upsert по id).
Author
Owner

Уточнение PO (важно): оцифрованные PDF после классификации содержат embedded attachment (напр. оригинал/скан, добавленный конвейером). Сейчас oo index извлекает текст только основного тела PDF.

Нужно: при извлечении текста для индекса oo_docs_text дополнительно обходить встроенные вложения PDF и индексировать их текст (и/или их содержимое, если это PDF/документ). Инструменты: pdfdetach -list/-saveall (poppler), при необходимости mutool, затем internal/docpipe (pdftotext/OCR) по вложениям. Индексировать по тому же file_id, склеивая текст тело+вложения (с маркером источника).

Критерий: oo search --content "<текст из вложения>" находит файл; и по шести номерам — где текст лежит только во вложении.

**Уточнение PO (важно):** оцифрованные PDF после классификации содержат **embedded attachment** (напр. оригинал/скан, добавленный конвейером). Сейчас `oo index` извлекает текст только основного тела PDF. Нужно: при извлечении текста для индекса `oo_docs_text` **дополнительно** обходить встроенные вложения PDF и индексировать их текст (и/или их содержимое, если это PDF/документ). Инструменты: `pdfdetach -list`/`-saveall` (poppler), при необходимости `mutool`, затем `internal/docpipe` (pdftotext/OCR) по вложениям. Индексировать по тому же `file_id`, склеивая текст тело+вложения (с маркером источника). Критерий: `oo search --content "<текст из вложения>"` находит файл; и по шести номерам — где текст лежит только во вложении.
Author
Owner

F6 — отчёт (продолжение): встроенные вложения PDF

PR #44 обновлён (та же ветка): #44
Ветка feat/pdf-content#42 · коммиты 3cc288d, ce4778b, 3fe43ee (base main).
Ребейз на main сделан (F2 #45, F4 #43). Не мержу и не закрываю.

Ребейз

  • Конфликты: .env.example (секции ES/SQL + наш own-индекс — свёл),
    cmd/oo/search.go (F4 вынес поиск в фасад c.Files().Search(); --backend own
    остался, для oo теперь фасад).
  • main не собирался после мержа F4+F2: ProviderPG объявлен дважды
    (file_facade.go и file_pg.go). Убрал дубликат в file_facade.go
    (константа осталась у SQL-стора); тестовый fakeStore переименован в
    textFakeStore (конфликт с фасадом). Отдельным коммитом ce4778b.
  • go build/vet/test -race — зелёные.

Что добавлено

  • internal/docpipe/pdfattach.go:
    • ListAttachments (pdfdetach -list), SaveAttachment (-save, безопасные
      имена без путей), JoinWithAttachments (маркер [attachment: <имя>]),
      xmlToText (снятие тегов/entity для XML/HTML), чтение .json/.csv.
    • ToMarkdownWithAttachments: тело через штатный ToMarkdown (OCR для
      сканов), затем вложения; нечитаемое — пропускается, нет pdfdetach/вложений —
      тело без падения.
  • file_text_index.go: дефолтный TextExtractor теперь зовёт
    ToMarkdownWithAttachments.
  • Tools + LookPath: добавлен pdfdetach.
  • Docs: docs/elasticsearch.md (раздел «Встроенные вложения PDF»), README,
    package-doc docpipe.

Вложение в индекс идёт под тем же file_id, текст склеен «тело + секции
вложений»; upsert идемпотентен.

Тесты (имена + PASS)

Unit (без сети):

  • docpipe: TestParseAttachmentList, TestParseAttachmentListEmptyAndMalformed,
    TestSafeAttachmentName, TestJoinWithAttachments,
    TestJoinWithAttachmentsNoAttachments, TestListAttachmentsWithoutTool,
    TestXMLToText, TestAttachmentMarkdownFallback,
    TestToMarkdownWithAttachmentsFixture (реальный pdfdetach+pdftotext на
    testdata/pdf-with-attachment.pdf), TestToMarkdownWithAttachmentsPlainPDF.
  • root: прежние TestESText*, TestTextIndexer* — PASS.
  • cmd/oo: TestIndex*, TestSplitList — PASS.

Integration (-tags=integration, skip без ONLYOFFICE_ES_URL):

  • TestIntegrationESTextIndex — PASS live.
  • TestIntegrationESTextIndexPDFAttachment — fixture → реальный конвейер
    (pdfdetach + pdftotext) → ES → поиск токена gooattachmenttoken, который
    есть только во вложении — PASS live.
  • TestIntegrationESSearch (регрессия F3) — PASS live.

go build/vet/test -race ./... — зелёные; go vet -tags=integration ./ —
чисто; gitleaks no leaks; po scan — clean. Фикстура
testdata/pdf-with-attachment.pdf (3 КБ, сгенерирована gs+pdfattach, без PII).

Живая проверка вложений (OO)

Реальный e-invoice: файл 21749 (2025-12-05-…E-INVOICE…pdf, папка 672) с
вложением factur-x.xml.

  • oo index files 21749 → в индексе маркер [attachment: factur-x.xml].
  • IBAN DE26330403100827038100 (есть только в XML, не в теле) →
    oo search "DE26330403100827038100" --content --backend own находит 21749;
    --backend oo (индекс OnlyOffice) — пусто.

Шесть номеров (папки 634/639, 22 PDF; вложений в этих PDF нет)

номер результат источник
S1019 3576 2026-07-28-112626-… тело
S1020 3577 2026-07-28-114826-… тело
S1021 3578 2026-07-28-S1021-… тело
S1030 3581 2026-07-28-S1030-… тело
S1055 3591 S1055.pdf + 3584 2026-08-20-S1055-… тело
S1063 не найден —

Проверил вложения всех 22 PDF и всего локального корпуса
(office-assistant/var, ~4750 PDF: 314 xml, 61 md, 61 yaml, 16 json) — ни один
не содержит S1019/S1020/S1021/S1030/S1055/S1063. S1063 нет и как файла.
Т.е. по этим шести «только во вложении» нет ни одного; вложения дают эффект на
других документах (e-invoices с factur-x.xml и оцифрованные PDF с <doc>.md).

Блокеры

Нет. Замечания:

  • .md-вложения (текст скана) и .xml (EN 16931) индексируются; .json
    (CuraSoft) — как текст (возможен шум токенов).
  • Индекс наполняется вручную (oo index), после изменений повторить
    (идемпотентно).
  • main был красный (дубль ProviderPG); фикс в этом PR — при желании вынесу
    отдельно.
## F6 — отчёт (продолжение): встроенные вложения PDF PR #44 обновлён (та же ветка): https://git.produktor.io/eSlider/go-onlyoffice/pulls/44 Ветка `feat/pdf-content#42` · коммиты `3cc288d`, `ce4778b`, `3fe43ee` (base `main`). Ребейз на `main` сделан (F2 #45, F4 #43). Не мержу и не закрываю. ### Ребейз - Конфликты: `.env.example` (секции ES/SQL + наш own-индекс — свёл), `cmd/oo/search.go` (F4 вынес поиск в фасад `c.Files().Search()`; `--backend own` остался, для `oo` теперь фасад). - `main` **не собирался** после мержа F4+F2: `ProviderPG` объявлен дважды (`file_facade.go` и `file_pg.go`). Убрал дубликат в `file_facade.go` (константа осталась у SQL-стора); тестовый `fakeStore` переименован в `textFakeStore` (конфликт с фасадом). Отдельным коммитом `ce4778b`. - `go build/vet/test -race` — зелёные. ### Что добавлено - `internal/docpipe/pdfattach.go`: - `ListAttachments` (`pdfdetach -list`), `SaveAttachment` (`-save`, безопасные имена без путей), `JoinWithAttachments` (маркер `[attachment: <имя>]`), `xmlToText` (снятие тегов/entity для XML/HTML), чтение `.json`/`.csv`. - `ToMarkdownWithAttachments`: тело через штатный `ToMarkdown` (OCR для сканов), затем вложения; нечитаемое — пропускается, нет pdfdetach/вложений — тело без падения. - `file_text_index.go`: дефолтный `TextExtractor` теперь зовёт `ToMarkdownWithAttachments`. - `Tools` + `LookPath`: добавлен `pdfdetach`. - Docs: `docs/elasticsearch.md` (раздел «Встроенные вложения PDF»), README, package-doc docpipe. Вложение в индекс идёт под тем же `file_id`, текст склеен «тело + секции вложений»; upsert идемпотентен. ### Тесты (имена + PASS) Unit (без сети): - docpipe: `TestParseAttachmentList`, `TestParseAttachmentListEmptyAndMalformed`, `TestSafeAttachmentName`, `TestJoinWithAttachments`, `TestJoinWithAttachmentsNoAttachments`, `TestListAttachmentsWithoutTool`, `TestXMLToText`, `TestAttachmentMarkdownFallback`, `TestToMarkdownWithAttachmentsFixture` (реальный pdfdetach+pdftotext на `testdata/pdf-with-attachment.pdf`), `TestToMarkdownWithAttachmentsPlainPDF`. - root: прежние `TestESText*`, `TestTextIndexer*` — PASS. - cmd/oo: `TestIndex*`, `TestSplitList` — PASS. Integration (`-tags=integration`, skip без `ONLYOFFICE_ES_URL`): - `TestIntegrationESTextIndex` — PASS live. - `TestIntegrationESTextIndexPDFAttachment` — fixture → реальный конвейер (pdfdetach + pdftotext) → ES → поиск токена `gooattachmenttoken`, который есть **только во вложении** — PASS live. - `TestIntegrationESSearch` (регрессия F3) — PASS live. `go build/vet/test -race ./...` — зелёные; `go vet -tags=integration ./` — чисто; gitleaks `no leaks`; `po scan` — clean. Фикстура `testdata/pdf-with-attachment.pdf` (3 КБ, сгенерирована gs+pdfattach, без PII). ### Живая проверка вложений (OO) Реальный e-invoice: файл **21749** (`2025-12-05-…E-INVOICE…pdf`, папка 672) с вложением `factur-x.xml`. - `oo index files 21749` → в индексе маркер `[attachment: factur-x.xml]`. - IBAN `DE26330403100827038100` (есть только в XML, не в теле) → `oo search "DE26330403100827038100" --content --backend own` **находит 21749**; `--backend oo` (индекс OnlyOffice) — пусто. ### Шесть номеров (папки 634/639, 22 PDF; вложений в этих PDF нет) | номер | результат | источник | |-------|-----------|----------| | S1019 | 3576 `2026-07-28-112626-…` | тело | | S1020 | 3577 `2026-07-28-114826-…` | тело | | S1021 | 3578 `2026-07-28-S1021-…` | тело | | S1030 | 3581 `2026-07-28-S1030-…` | тело | | S1055 | 3591 `S1055.pdf` + 3584 `2026-08-20-S1055-…` | тело | | S1063 | не найден | — | Проверил вложения всех 22 PDF и всего локального корпуса (`office-assistant/var`, ~4750 PDF: 314 xml, 61 md, 61 yaml, 16 json) — ни один не содержит S1019/S1020/S1021/S1030/S1055/S1063. S1063 нет и как файла. Т.е. по этим шести «только во вложении» нет ни одного; вложения дают эффект на других документах (e-invoices с `factur-x.xml` и оцифрованные PDF с `<doc>.md`). ### Блокеры Нет. Замечания: - `.md`-вложения (текст скана) и `.xml` (EN 16931) индексируются; `.json` (CuraSoft) — как текст (возможен шум токенов). - Индекс наполняется вручную (`oo index`), после изменений повторить (идемпотентно). - `main` был красный (дубль `ProviderPG`); фикс в этом PR — при желании вынесу отдельно.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: eSlider/go-onlyoffice#42