同樣叫做 abstract 的欄位,裝進去的內容卻可能不同。回頭讀 ResearchForge 在 2026 年 6 月 26 日的搜尋程式,我看到 Crossref 的摘要會先清除標記,OpenAlex 的摘要要依詞語位置重組,Semantic Scholar 則在摘要缺少時,改取 tldr.text 的文字。[1] 對下游來說,它們最後都是一個字串;但字串相同的資料型別,並沒有交代相同的內容來歷。
上一篇處理查詢如何保留題目的範圍。查詢送出去後,我接著要面對的是接收端:OpenAlex、Crossref 與 Semantic Scholar 都提供學術資料,回來的欄位與缺值卻各有形狀。如果直接交給畫面處理,來源卡片就得自己理解三套回應;之後每個使用來源的地方,也可能各自做出不同解釋。
這篇把時間停在當時的程式版本,追查三個服務如何進入共同紀錄。問題不只是把欄位名稱對齊,還包括轉換後留下什麼、捨棄什麼,以及下游究竟能相信到哪一步。
當時的 provider 目錄確實有這三個服務的檔案,不過內容主要是名稱、啟用設定與連線位置。真正把回應轉成來源紀錄的邏輯,位於搜尋服務裡,分別由三個轉換函式處理。[1] 只看目錄,容易把「已有設定」讀成「這裡已經完成整套串接」。
我把這類邊界轉換稱為 adapter:它接住外部格式,輸出程式內部約定的格式。ResearchForge 當時使用 SourceRecord,也就是包含題名、作者、年份、摘要、DOI 與來源服務等欄位的共同紀錄。DOI 是用來識別文獻的識別碼;它可以幫忙比對資料,但有 DOI 仍不表示程式已讀過文獻。
三個轉換函式都先取得題名。沒有可用題名,就回傳 null,表示這筆資料不進入後面的來源清單;有題名時,才整理其他欄位並交給共同的正規化函式。[1] 這是一道很窄的入口條件:它要求紀錄能被辨認,並沒有判斷研究內容是否支持使用者的問題。
Crossref 的作者資料可能有完整姓名,也可能拆成名字與姓氏;當時的轉換先取完整姓名,否則才組合兩個部分。年份則依程式設定的順序,從印刷、線上、發行等日期欄位尋找。[1] 這些選擇讓下游能得到一致格式,也把原本多個日期縮成一個年份。
OpenAlex 的處理更能看出 adapter 的責任。程式接收的是「詞語對應出現位置」的索引,因此必須展開詞語與位置、依位置排序,再接成文字。這段工作放在來源邊界後,顯示卡片的元件就不必知道摘要原先怎麼排列。歷史實作最後的組合步驟如下:[1]
const restored = wordsByPosition
.sort((a, b) => a.position - b.position)
.map((item) => item.word)
.join(" ")
.replace(/\s+([,.;:!?%)])/g, "$1")
.replace(/([(])\s+/g, "$1")
.trim();
return restored || null;
這段程式是在重組已取得的文字。沒有索引,或組不出文字時,就保留缺值;它不會替文獻寫一段摘要。如此一來,資料不足至少仍能在紀錄中看見,不必為了讓畫面填滿而補出內容。
Semantic Scholar 的轉換則多了一項取捨:優先使用摘要,沒有時改取 tldr.text。兩種文字都進入 abstract,這段轉換沒有另外記下用了哪個分支。[1] 若下游只看到非空摘要,就推定它一定是原始摘要,已經比程式實際保存的資訊多走了一步。
保存來源來歷,也需要說清楚保存到哪一層。三個轉換函式都帶入 provider 名稱、查詢文字與取得時間;這些資訊能幫助追查資料從哪個服務而來,卻不等於逐一保存每個欄位的所有原始版本。[1]
同一時期新增的 NormalizedSource 是研究流程使用的另一種共同紀錄。它能從 SourceRecord 帶入來源、查詢、題名等資訊,整理 DOI 與文字,並產生供去重使用的鍵值。不過,保存原始回應的欄位在沒有另外提供時預設為 null。[2] 資料模型容得下原始內容,與這次轉換確實保存了它,是兩件需要分別核對的事。
共同格式的收益很實際:後續流程可以使用同一組欄位,不必重做作者拆分或摘要重組。代價是,一旦把不同來歷的值放進同一欄,差異就可能被壓縮。我現在會先問下游需要做什麼判斷,再決定要保存哪些轉換痕跡;若需要區分摘要與替代短文,就不能只留一個字串讓它猜。
資料形狀之外,當時的搜尋服務也把單一 provider 的結果包成共同回應,包含是否成功、來源服務、查詢、結果清單與錯誤資訊。個別請求的錯誤會被接成失敗回應,讓多服務搜尋能取得各自的結果。[1]
成功回應仍可以帶著空清單。程式中的 ok: true 表示這條呼叫路徑沒有在此處被判成失敗,不能直接翻譯成「已找到可用文獻」。全部 provider 都失敗時,彙整函式才拋出整體錯誤;只要有成功回應,就把成功部分的來源合併、去重,再限制回傳筆數。[1]
因此,假設一個服務失敗、另一個成功但沒有資料,彙整仍可能得到空清單。這是依控制流程推得的情境,不是一次歷史線上事故。它提醒我:要說明來源為何不足,得保留各服務的結果狀態;只看最後清單,無法知道空白是怎麼來的。
三個搜尋轉換函式產生的 SourceRecord,審查狀態都先設為 pending,也就是等待審查。[1] 題名、年份與摘要已整理好,只代表資料可以進入共同流程,還沒有因此取得被採用的資格。
審查狀態還有另一條值得追查的轉換路徑。同一版本從 NormalizedSource 轉回 SourceRecord 的另一個函式,會直接填入 accepted。[2] 光是讀到這項指定,不能推論所有搜尋結果都被自動接受;但它足以提醒我,若要確認審查狀態怎麼改變,就必須追查實際走過哪個轉換,不能假設每個入口都遵守相同條件。
這次程式核對能說明資料與狀態如何被整理,無法證明三個服務在任意題目下都可靠,也沒有建立文獻內容正確的保證。對多來源系統而言,這樣的限制應該留在設計判斷裡,不能被整齊的卡片畫面遮住。
若要把這個經驗用在自己的專案,我會挑一個下游最依賴的欄位,例如摘要,逐一追查每個來源的取值、缺值與替代路徑。接著問:接收端需要知道這些差異嗎?現在的紀錄是否足以回答?這比先堆出一份很完整的共同型別,更容易找到真正需要補上的資訊。
三份回應能說同一種資料語言後,仍可能指向同一篇文獻。下一個要處理的問題便是:它們究竟是三筆紀錄,還是同一份研究的三個入口?
[1] ResearchForge 專案歷史,2026 年 6 月 26 日版本:provider 設定、來源搜尋服務與三種回應轉換實作。私人專案程式,本文僅節錄必要片段。
[2] ResearchForge 同日研究資料正規化實作:SourceRecord 與 NormalizedSource 的雙向轉換。作為資料與狀態指定的依據,不代表線上驗收。
延伸閱讀:Crossref REST API 文件、OpenAlex 說明中心。兩者供讀者查閱服務文件,不作為 ResearchForge 歷史行為的證明。