我在固定資料測試裡放了兩筆來源:一筆來自 Crossref,另一筆標示 Semantic Scholar;題名、網址與資料完整度不同,卻有同一個 DOI。去重後,輸出剩一筆,另一筆較豐富的摘要也被保留下來。[1]
這不是線上搜尋的實際成果,而是 2026 年 6 月 26 日歷史程式裡的 fixture,也就是專為固定情境準備的測試資料。它讓我看見一個容易被「刪除重複卡片」遮住的問題:去重要先回答的,不是兩個字串像不像,而是兩筆紀錄是否在描述同一份研究。
Day 07 處理了三種 provider 如何轉成共同資料形狀。但格式統一後,同一篇文獻反而更容易被當成數筆獨立資料。如果這些重複紀錄一起進入排序與後續處理,「找到幾筆」就會和「辨認出幾份研究」混在一起。
當時的 ResearchForge 同時有 SourceRecord 與 NormalizedSource 兩種資料模型。前者承接來源搜尋與審查所需的欄位;後者把 DOI、題名、作者與年份整理成研究流程可比較的形狀。歷史搜尋路徑會先處理 SourceRecord 的重複;另一條研究路徑則先轉成 NormalizedSource,再去重、計算相關性與來源品質分數。[1][2]
這個順序很重要。如果先為三筆實為同一研究的紀錄各算一次分數,後面才合併,中間結果仍可能受到重複輸入影響。歷史研究路徑選擇先 dedupe、後 scoring,能證明的只是處理順序;它不能自動證明每一次身分判斷都正確。
我也必須把「辨認成同一份研究」與「允許這份研究進入報告」分開。前者是 identity 判斷,後者還要處理相關性、來源品質與審查狀態。去掉重複不會把候選來源變成證據;它只是讓後續決定面對較清楚的單位。
在 dedupeNormalizedSources 裡,每筆紀錄會先建立分組用的 key。以下是當時程式的連續片段:
const doi = normalizeDoi(source.doi);
const titleKey = normalizeTitleKey(source.title);
const authorKey = firstAuthorKey(source.authors);
const key = doi || [titleKey, authorKey, source.year ?? ""].join("|");
有 DOI 時,程式優先用整理後的 DOI;沒有時,才把題名、第一作者與年份組成 key。[2] 這也說明「沒有 DOI」不能直接等於「無法去重」,但替代訊號越弱,誤判的空間就越大。
在轉進 NormalizedSource 之前,SourceRecord 的正規化還會移除 DOI 網址前綴、清理空白並轉為小寫。[1] 所以比對的不是 provider 原樣傳回的字串。反過來說,當時的這支 NormalizedSource deduper 並沒有把 provider 自己的 id 當成跨來源的優先分組 key;系統保留 id 來追蹤來源,不等於它能單獨證明跨 provider 身分。[2][3]
當時的規則可整理成這張表:
| 判斷階段 | 使用的訊號 | 當時的行為 |
|---|---|---|
| 直接分組 | 正規化 DOI | 相同 key 放入同一組 |
| 無 DOI 分組 | 題名 key+第一作者+年份 | 組合成取代 key |
| 相似比對 | 題名 token 相似度、第一作者、接近年份 | 達到歷史門檻就併入既有組 |
第三列不是學術界的通用真理,只是這個版本的工程規則。程式把題名拆成 token 集合計算相似度,再搭配首位作者與接近年份決定是否併組。[2] 門檻是當時原碼的實作細節,這次沒有線上樣本能證明它是最佳值,因此公開文章不把它寫成可通用的建議。
我原先把弱訊號理解成「possible match,交給後續規則或人工檢查」。但重讀歷史實作後,這句話不能留下:這個 deduper 沒有三態輸出。相似條件成立時,它直接併入某個已有分組;不成立才開新組。[2]
這是一個真正的限制。關鍵字相似可能是同一研究的標題變體,也可能是同一作者在相近年份發表的不同工作。當系統只輸出「已合併」或「未合併」,錯合併後的資料會看起來更完整,反而比沒有去掉的重複卡片更難察覺。
還有一個必須如實保留的程式可能性:歷史函式在沒有找到相同 key 後,進入相似比對時並未先排除「兩邊 DOI 都存在但不同」的情境。因此,不同 DOI 的紀錄在弱訊號條件都成立時,仍有可能被併入同組。[2] 這是對控制流程的推論,不是一次已觀察到的線上錯誤。
缺值也不是無影響的空白。在這個相似比對裡,第一作者經過整理後相同是條件之一;當兩邊都沒有可用作者時,實作上兩個空值仍會相等。[2] 這不表示一定會錯合併,因為還有題名與年份條件;但它提醒我,「欄位缺失」本身也應該有明確的比對語意。
現在回頭看,如果弱訊號要直接改變身分,至少應保留命中的規則、相似度與原始成員,並用「很像但不是同一篇」的 hard negative 測試它。這是我從原碼邊界得到的設計反思,不是當時已完成的功能。
去重不只有「是否同一篇」這個決定。同組資料還要決定後續使用哪一筆作為主體。NormalizedSource 的歷史 deduper 以 metadata completeness 計分挑選較完整的紀錄,其中 DOI、摘要、作者、年份與期刊等欄位會影響選擇。其他紀錄的 provider、id,以及可用的 DOI 或 URL,則放入 mergedFrom。[2][3]
這個設計保留了「還有哪些發現路徑」,但它不是逐欄位保留所有版本的衝突表。選出較完整的主體,不等於證明那筆的每個值都比較正確。而且 mergedFrom 記錄了次要來源身分,當時卻沒有一併寫入「這次為什麼合併」的正式理由欄位。[3]
還有一個小而關鍵的邊界:完整度相同時,比較函式會保留先進入的那筆。[2] 這代表輸入順序至少在同分情境中會影響代表紀錄。只要後續有人把「被選中的主體」誤當成「權威來源」,一個本來只是程式比較的 tie-break,就會被誤解成品質結論。
另一邊的 SourceRecord deduper 會保留較長摘要、合併作者與部分 metadata;固定資料測試也檢查了 DOI 重複和無 DOI 時的題名+首位作者+年份情境。[1] 這兩個實作有不同的合併行為,也正是不能只說「系統有去重」的原因:還要知道資料正在哪一層,以及那一層如何合併。
第一,單位是 provider record、研究作品,還是一個可下載檔案?這篇的歷史邊界只討論前兩者。第二,哪些訊號能直接合併,哪些只能提示相似?第三,缺值如何影響判斷?第四,合併後留下哪筆欄位,與身分判斷是不是兩個獨立決定?第五,我能不能回答每一次合併的來源與理由?
把這五問寫進資料契約與測試,比單純調整一個相似度門檻更穩定。我也會特別測順序不同時的結果:如果只因 provider 輸入順序不同,代表紀錄或來源軌跡就改變,那也是 identity 規則的一部分,不能略過。這些是給新系統的測試建議,不是歷史版本已通過的驗收項目。
當資料清單不再被重複紀錄擴張,我才有一組較穩定的比較對象。但辨認出不同研究,仍不代表它們真的回答了原題。Day 09 會把焦點從 identity 移到 relevance:為什麼每個字都看似相關,整篇卻可能離題。
[1] ResearchForge 專案歷史,2026 年 6 月 26 日:來源搜尋、SourceRecord 去重邏輯與固定資料測試。
[2] ResearchForge 同日研究路徑:SourceRecord 轉為 NormalizedSource、去重後再進行 relevance、quality 與 final score。
[3] ResearchForge 同日的研究去重模組與資料模型:metadata completeness、mergedFrom 與來源保留邊界。