ANSWERS · FILES AND FORMATS

Why does my Notion export duplicate titles?

AUTHOR · XERJ documentation team · REVIEWED BY · XERJ engineering team · PUBLISHED · 2026-08-21 · UPDATED · 2026-10-30
SHORT ANSWER · UPDATED 2026-10-30

Why does my Notion export duplicate titles in search results? The duplicates come from the export layout and from the 2 documents XERJ writes per Markdown file, not from re-indexing. A captured run indexed the same Notion export twice and returned 8 documents both times, with byte-identical ids, so the index never doubled.

Key number: Re-indexing the same Notion export a second time returned the same 8 documents, with byte-identical ids. (as of 2026-08-21 · source: the captured run documented on this page)

TL;DR The duplicates come from the export layout and from the 2 documents XERJ writes per Markdown file. Re-indexing is not the cause: the same export indexed twice returned 8 documents both times, with byte-identical ids.

Recorded run: documents per exported file

8 documents, and where each one comes from

Markdown pageDatabase CSV

  • Engineering Handbook <32-hex>.md2
  • Engineering Handbook .../Runbooks <32-hex>.md2
  • 3 database row pages in Decisions <id>/0
  • Decisions <id>_all.csv4
each Markdown page2 documentsOne whole-file document, ax_locator file, and one passage, s0. Same ax_path, same title text.
the row pagesRefused as yamlTitle over Status:, Owner: and Decided: lines. The run reported junk_files=3. The CSV keeps the rows as r0, r1 and r2.
Bars are drawn to scale against 4. Counts from RUN-B, captured on 2026-08-21 on an export folder written to Notion's file naming by the fixture generator.
PASTE THIS TO YOUR AI AGENT — IT DOES THE REST Act as a coding agent. Read https://xerj.org/llms.txt, start a local XERJ node, unzip a Notion Markdown and CSV workspace export, run `xerj autoindex ./notion-export --url http://127.0.0.1:9200 --prefix nt --progress plain` twice, POST a match_all sorted by ax_path and ax_locator after each run, compare the returned _id lists byte for byte, and report every file the autoindex-catalog marked junk with its reason.
RUN THIS XERJ COMMAND xerj autoindex ./notion-export --url http://127.0.0.1:9200 --prefix nt --progress plain Index the unzipped Notion export. Run the same line twice to test for duplicates.
RUN THIS XERJ COMMAND curl -s -XPOST http://127.0.0.1:9200/nt-*/_search -H 'content-type: application/json' -d '{"query":{"match_all":{}},"size":100,"_source":["ax_path","ax_format","ax_locator"],"sort":[{"ax_path":"asc"},{"ax_locator":"asc"}],"track_total_hits":true}' List every document with its path and locator, in a stable order, after each run.
RUN THIS XERJ COMMAND curl -s -XPOST http://127.0.0.1:9200/nt-*/_search -H 'content-type: application/json' -d '{"query":{"match_phrase":{"body":"meerkat quarterly roadmap"}},"size":10,"_source":["ax_path","ax_format"],"track_total_hits":true}' Find a phrase that Notion exported into 2 different pages.
RUN THIS XERJ COMMAND curl -s -XPOST http://127.0.0.1:9200/autoindex-catalog/_search -H 'content-type: application/json' -d '{"query":{"term":{"doc_kind":"file"}},"size":500,"_source":["path","format","status","records","reason"],"sort":[{"path":"asc"}]}' List every exported file with its family, status and refusal reason.

The export layout repeats the title

Notion writes a page as <Title> <32-hex>.md and its children into a sibling folder named <Title> <32-hex>/. One title then shows up in a file name and in a folder name.

In the run, a phrase that Notion copied into a parent page and a child page returned 2 hits, one in each file. Both hits are correct, because the text is in 2 exported files. The third command above is a search of that kind.

Each Markdown page is 2 documents

XERJ writes 1 whole-file document plus 1 document per passage, so a short page is a pair with the same ax_path. Group on ax_path in your client for 1 result per page:

Collapse the pairs in your client

4 documents, 2 pages

As returned

ax_pathax_locator
Engineering Handbook a1b2c3d4e5f60718293a4b5c6d7e8f90.mdfile
Engineering Handbook a1b2c3d4e5f60718293a4b5c6d7e8f90.mds0
Engineering Handbook .../Runbooks a1b2c3d4e5f60718293a4b5c6d7e8f90.mdfile
Engineering Handbook .../Runbooks a1b2c3d4e5f60718293a4b5c6d7e8f90.mds0

Sorted on ax_path, then ax_locator, so each pair sits together.

Grouped on ax_path

pagedocuments
Engineering Handbook a1b2c3d4e5f60718293a4b5c6d7e8f90.md2
Engineering Handbook .../Runbooks a1b2c3d4e5f60718293a4b5c6d7e8f90.md2

One result per page. Both pages are real: Notion wrote the same title into a parent and a child.

The 4 Markdown documents from the match_all listing in the recorded run. Grouping happens in your client, not in the index.

The second command above is the listing query, sorted on ax_path and then ax_locator.

Re-indexing is idempotent

The run indexed the export, then indexed the same folder again with the same command. Both runs returned 8 documents, every _id matched byte for byte, and the second run added nothing. Its terminal line read files=0 records=8: every file was unchanged, so no new source documents went in.

Database row pages can be refused as YAML

A Notion database row page is a title over Status:, Owner: and Decided: lines. When most non-blank lines of a file read as YAML, the sniffer gives it the yaml family. The YAML extractor got nothing from these pages, so each was dropped with 0 documents and no fallback to prose. Front matter alone does not do this, because a file that opens with --- is classified by the body under it.

All 3 row pages were refused with the reason no records extracted (yaml candidate family, 1 junk lines), and the terminal only showed junk_files=3. A 3-file reproduction outside Notion did the same: prose-no-colons.txt was indexed as txt-prose, and speaker-colons.txt and markdown-bullets.md were dropped as yaml.

This run predates a sniffer fix closed on 2026-10-04 (issue #1142). A file now also has to parse as a YAML stream before it gets the yaml family, so labeled prose that does not parse is indexed as prose. Re-run and read autoindex-catalog to see how your own row pages land.

The database CSV keeps the rows

Notion also writes each database to <Name> <id>_all.csv. XERJ indexed it as csv with 4 documents: 1 whole-file document and 3 rows at r0, r1 and r2. Read autoindex-catalog after every run with the last command above, and compare the file count with the document count before you report either.

What this capture does not show

The export folder came from the fixture generator, written to Notion's own file naming. Notion never ran on the host and no workspace was connected. XERJ has no Notion connector and fetches nothing over the network.

XERJ ran single-node, with no replication and no failover. The default embedder is lexical feature hashing, so a paraphrase that shares no words with the query does not match. Neural embeddings are opt-in through --embed-mode neural.

Every number here comes from RUN-B, captured on 2026-08-21 on a 16-core AMD EPYC 9645 host with a ci-test profile build, so no wall-clock figure is published as a performance number.

FAQ

Does indexing a Notion export twice create duplicates?

No. The captured run indexed the same export twice and got 8 documents both times, with byte-identical ids and nothing added.

Why do I see 2 results for the same Markdown page?

Each Markdown file produced 2 documents: one whole-file document with ax_locator file, and one passage document with ax_locator s0.

Why does one title appear in 2 different paths?

Notion writes a page as <Title> <32-hex>.md and its children into a sibling <Title> <32-hex>/ directory. Text copied into both pages therefore matches twice.

Why did my Notion database rows return nothing?

All 3 database row pages were classified as yaml and refused with 0 documents. A prose file whose non-blank lines mostly read as key: value can be dropped this way.

How do I collapse duplicate hits into 1 page?

Group the returned hits on ax_path in your client. The captured listing query sorts on ax_path and then ax_locator, which makes the pairs visible.

Does the CSV that Notion writes for a database work?

Yes. The captured run indexed Decisions <id>_all.csv as csv with 4 documents: 1 whole-file document and 3 row documents.

Where do these results come from?

All results come from run RUN-B, captured on 2026-08-21 on a 16-core AMD EPYC 9645 host.

Related