Skip to content

Commit 7fb8432

Browse files
authored
@ (#54)
Add explicit title override to document ingest Accept an optional "title" on ingest (multipart form field or JSON body). When set it seeds the document row AND is threaded through the ingest Payload so persistTree keeps it — the parsed title never clobbers it. Motivation: some PDFs have unreliable title text the parser latches onto — rotated arXiv margin stamps extracted in reverse ("3202 guA" for "Aug 2023"), repeated ORCID iD markers ("iD iD iD iD"), letter-spaced cover pages. The existing mojibake guard only catches doubled-glyph watermarks. An explicit title lets callers (and curated corpora like the playground demo set) show a clean display name regardless. Empty = keep auto-discovery. @
1 parent db327d3 commit 7fb8432

3 files changed

Lines changed: 73 additions & 15 deletions

File tree

internal/handler/documents.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ func (h *DocumentsHandler) HandleIngestDocument(w http.ResponseWriter, r *http.R
178178
}
179179

180180
var (
181-
filename string
182-
contentType string
183-
body io.Reader
184-
size int64
181+
filename string
182+
contentType string
183+
body io.Reader
184+
size int64
185+
titleOverride string
185186
)
186187

187188
ct := r.Header.Get("Content-Type")
@@ -201,12 +202,15 @@ func (h *DocumentsHandler) HandleIngestDocument(w http.ResponseWriter, r *http.R
201202
contentType = header.Header.Get("Content-Type")
202203
body = file
203204
size = header.Size
205+
// Optional multipart "title" field overrides the discovered title.
206+
titleOverride = strings.TrimSpace(r.FormValue("title"))
204207

205208
case strings.HasPrefix(ct, "application/json"):
206209
var payload struct {
207210
Filename string `json:"filename"`
208211
ContentType string `json:"content_type"`
209212
Content string `json:"content"`
213+
Title string `json:"title"`
210214
}
211215
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
212216
writeErr(w, http.StatusBadRequest, "invalid json: "+err.Error())
@@ -220,6 +224,7 @@ func (h *DocumentsHandler) HandleIngestDocument(w http.ResponseWriter, r *http.R
220224
contentType = payload.ContentType
221225
body = strings.NewReader(payload.Content)
222226
size = int64(len(payload.Content))
227+
titleOverride = strings.TrimSpace(payload.Title)
223228

224229
default:
225230
writeErr(w, http.StatusUnsupportedMediaType,
@@ -240,7 +245,10 @@ func (h *DocumentsHandler) HandleIngestDocument(w http.ResponseWriter, r *http.R
240245
return
241246
}
242247

243-
title := filename
248+
title := titleOverride
249+
if title == "" {
250+
title = filename
251+
}
244252
if title == "" {
245253
title = string(docID)
246254
}
@@ -281,6 +289,7 @@ func (h *DocumentsHandler) HandleIngestDocument(w http.ResponseWriter, r *http.R
281289
ContentType: contentType,
282290
Filename: filename,
283291
SourceRef: key,
292+
Title: titleOverride,
284293
Profile: r.Header.Get("X-Vectorless-Profile"),
285294
})
286295
if err := h.queue.Enqueue(ctx, queue.Job{

pkg/ingest/ingest.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ type Payload struct {
6767
ContentType string `json:"content_type"`
6868
Filename string `json:"filename"`
6969
SourceRef string `json:"source_ref"` // storage key of the original bytes
70+
// Title, when set, is an explicit caller-supplied document title that
71+
// overrides whatever the parser discovers from the content. It is
72+
// "sticky": persistTree will NOT overwrite it with the parsed title.
73+
// Useful when the PDF's own title text is unreliable (rotated arXiv
74+
// margin stamps, ORCID iD markers, letter-spaced cover pages) or the
75+
// caller simply wants a curated display name. Empty = auto-discover.
76+
Title string `json:"title,omitempty"`
7077
// Profile selects domain-aware structuring/summarization prompts
7178
// ("generic", "research", "medical"). Empty = generic. Sourced from
7279
// the document's store (the control plane injects X-Vectorless-Profile).
@@ -376,7 +383,7 @@ func (p *Pipeline) Run(ctx context.Context, pl Payload) error {
376383
}
377384
log.Info("ingest: parsed", "sections", len(parsed.Flatten()), "title", parsed.Title)
378385

379-
if err := p.persistTree(ctx, p.DB, pl.DocumentID, parsed); err != nil {
386+
if err := p.persistTree(ctx, p.DB, pl.DocumentID, parsed, pl.Title); err != nil {
380387
p.fail(ctx, p.DB, pl.DocumentID, "persist tree", err)
381388
return err
382389
}
@@ -674,7 +681,7 @@ func (p *Pipeline) runMinimal(ctx context.Context, store docPersister, pl Payloa
674681
}
675682
log.Info("ingest: parsed", "sections", len(parsed.Flatten()), "title", parsed.Title)
676683

677-
if err := p.persistTree(ctx, store, pl.DocumentID, parsed); err != nil {
684+
if err := p.persistTree(ctx, store, pl.DocumentID, parsed, pl.Title); err != nil {
678685
p.fail(ctx, store, pl.DocumentID, "persist tree", err)
679686
return err
680687
}
@@ -695,13 +702,18 @@ func (p *Pipeline) runMinimal(ctx context.Context, store docPersister, pl Payloa
695702
// The DB operations go through the narrow docPersister interface so the
696703
// persist path can be exercised (e.g. by the minimal-mode test) without
697704
// a live Postgres; production callers pass p.DB, which satisfies it.
698-
func (p *Pipeline) persistTree(ctx context.Context, store docPersister, docID tree.DocumentID, doc *parser.ParsedDoc) error {
699-
// Only overwrite the row's title (which was seeded with the
700-
// filename at upload time) when the parsed title looks usable.
701-
// Watermarked PDFs whose overlay text shares a Y coordinate with
702-
// the real title produce mojibake like "GGlloobbaall SSttrraatteeggyy"
703-
// — we'd rather keep the original filename than show that to a user.
704-
if doc.Title != "" && !isLikelyMojibakeTitle(doc.Title) {
705+
func (p *Pipeline) persistTree(ctx context.Context, store docPersister, docID tree.DocumentID, doc *parser.ParsedDoc, titleOverride string) error {
706+
// An explicit caller-supplied title is sticky: keep it and never let
707+
// the parsed title clobber it. The row was already seeded with this
708+
// value at upload time, so there is nothing to write here.
709+
if strings.TrimSpace(titleOverride) != "" {
710+
// no-op: the override is already the row's title.
711+
} else if doc.Title != "" && !isLikelyMojibakeTitle(doc.Title) {
712+
// Otherwise only overwrite the row's title (seeded with the
713+
// filename at upload time) when the parsed title looks usable.
714+
// Watermarked PDFs whose overlay text shares a Y coordinate with
715+
// the real title produce mojibake like "GGlloobbaall SSttrraatteeggyy"
716+
// — we'd rather keep the original filename than show that to a user.
705717
if err := store.SetDocumentTitle(ctx, docID, doc.Title); err != nil {
706718
return err
707719
}

pkg/ingest/persist_content_ref_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ import (
99
"github.com/hallelx2/vectorless-engine/pkg/storage"
1010
)
1111

12+
// TestPersistTree_TitleOverrideIsSticky verifies that an explicit
13+
// caller-supplied title is never clobbered by the parsed title, while a
14+
// blank override still lets a usable parsed title through.
15+
func TestPersistTree_TitleOverrideIsSticky(t *testing.T) {
16+
store, err := storage.NewLocal(t.TempDir())
17+
if err != nil {
18+
t.Fatalf("NewLocal: %v", err)
19+
}
20+
p := &Pipeline{Storage: store}
21+
doc := &parser.ParsedDoc{
22+
Title: "Some Parsed Title",
23+
Sections: []parser.Section{
24+
{Level: 1, Title: "S", Content: "body", PageStart: 1, PageEnd: 1},
25+
},
26+
}
27+
28+
// With an override, persistTree must NOT push the parsed title (the row
29+
// already carries the override from upload time → SetDocumentTitle
30+
// stays uncalled, so the fake's title remains empty).
31+
fake := &fakeDocStore{}
32+
if err := p.persistTree(context.Background(), fake, "doc_x", doc, "Attention Is All You Need"); err != nil {
33+
t.Fatalf("persistTree (override): %v", err)
34+
}
35+
if fake.title != "" {
36+
t.Errorf("override present: parsed title must not overwrite it; SetDocumentTitle called with %q", fake.title)
37+
}
38+
39+
// With no override, a usable parsed title IS applied.
40+
fake2 := &fakeDocStore{}
41+
if err := p.persistTree(context.Background(), fake2, "doc_y", doc, ""); err != nil {
42+
t.Fatalf("persistTree (no override): %v", err)
43+
}
44+
if fake2.title != "Some Parsed Title" {
45+
t.Errorf("no override: parsed title should apply, got %q", fake2.title)
46+
}
47+
}
48+
1249
// TestPersistTree_ContentRefMatchesStoredObjects is the HAL-316 regression:
1350
// a leaf only gets a ContentRef when its content was actually written. An
1451
// empty-after-clean leaf must get NO ref (and no stored object), so later
@@ -35,7 +72,7 @@ func TestPersistTree_ContentRefMatchesStoredObjects(t *testing.T) {
3572
},
3673
}
3774

38-
if err := p.persistTree(context.Background(), fake, "doc_x", doc); err != nil {
75+
if err := p.persistTree(context.Background(), fake, "doc_x", doc, ""); err != nil {
3976
t.Fatalf("persistTree: %v", err)
4077
}
4178

0 commit comments

Comments
 (0)