2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustで学術論文からテキスト抽出するクレートを実装するAdvent Calendar 2024

Day 15

Rustで学術論文からテキストを抽出する #15 応用編 - arXiv論文収集システム構築③ SemanticScholar APIラッパーの実装

Last updated at Posted at 2024-12-14

Summary

  • Semantic ScholarのAPIラッパーを実装しました
    • まずは論文のID検索と論文の詳細情報を取得できるAPIを実装
    • リファレンスや引用論文,著者情報などに関するAPIは後日実装

Crates & Repositories

crate GitHub
rsrpp rsrpp
rsrpp-cli rsrpp
arxiv-tools rs-arxiv-tools
ss-tools rs-ss-tools

前回までのあらすじ

前回:Rustで学術論文からテキストを抽出する #14

前回はarXivのAPIラッパーを実装して,日次で最新の論文情報を取得できるようにしました.
今回はさらに,Semantic Scholaから論文の情報を取得できるようにします.

image.png

なぜSemantic Scholarが必要か?

スクリーンショット 2024-12-14 22.54.54.png

arXivで論文の情報は取れているはずなのに,なぜ追加でSemantic Scholarが必要なのか?
その理由は.Semantic ScholarがarXivにはない重要な情報をいくつか提供してくれているからです.

具体的には,以下が欲しい.

  • ある論文を引用している論文のメタ情報
  • ある論文が参照している論文のメタ情報
  • 著者の情報 (特にh-indexとか)
  • (Influential Citation Count)

論文の情報を分析するときには,論文のテキストだけではなく,その論文がどんな論部を引用しているか,またどんな論文に引用されているかというCitation Networkの情報が非常に重要です.
毎日上がってくる玉石混交な論文の中から重要な論文を選ぶだけであればここまでは必要ないのですが,特定の領域について深く探究したい場合には,その領域全体の論文の配置がどのようになっているのかを知りたいです.

Citation Networkはそのためにうってつけの情報源なので,「玉」の論文を仕分けた後のNext Actionに必要な情報として,Semantic Scholarから情報検索します.

image.png

著者情報やInfluential Citation Countはあると便利な参考情報です.

実装すること

大きな構造は前回のarixv-toolsと同じです.
今回はAPIのエンドポイントが複数あることと,APIキーを使用することができるという点が異なります.

Semantic Scholar APIの使い方はこちらのチュートリアルを参照すればOK.
ただし,APIラッパーの実装にあたってはAPIの仕様が必要なので,それはこちらを参照します.

Semantic Scholarには用途によって幾つかのエンドポイントが用意されています.

EndPoint 用途 Method
/paper/autocomplete Suggest paper query completions GET
/paper/batch Get details for multiple papers at once POST
/paper/search Paper relevance search GET
/paper/search/bulk Ppaer bulk search GET
/paper/search/match Paper title search GET
/paper/{paper_id} Details about a paper GET
/paper/{paper_id}/authors Details about a paper's authors GET
/paper/{paper_id}/citations Details about a paper's citations GET
/paper/{paper_id}/references Details about a paper's references GET
/author/batch Get details for multiple authors at once POST
/author/search Search for authors by name GET
/author/{author_id} Details about an author GET
/author/{author_id}/papers Details about an author's papers GET

今回主に使用するのは,/paper/search/match および /paper/{paper_id} の2つです.
前者は論文のタイトルからSemantic Scholarの論文IDを検索するために使用します.
検索した論文IDを使用して,後者のAPIで論文の詳細情報を取得します.

第13回 で少し触れましたが,この/paper/search/match APIはクエリに与えた文字列と関連が深い論文をスコア順に複数出力します.そのため,返ってきた論文のリストの中から探しているタイトルの論文を抽出する必要があります.
一応,SemanticScholarの出力でも関連度のスコアを出してくれてはいるのですが,稀に全く異なるタイトルのスコアが高くなることがあるので,きちんとタイトルを使って判別した方が安全です.

今回は,まずはAPIのラッパーを実装することに注力するのでとりあえずリストの先頭の論文を返すようにしていますが,どこかでタイトルの類似度を計算する機能を追加しようと思います.

今回のAPIの完成系イメージはこちら.arXivのときとは少しUIが異なりますが,実装の規模は同程度になるように作ります.

let query_text = "attention is all you need";

let mut ss = SemanticScholar::new();
let paper_id = ss.query_paper_id(query_text.to_string()).await;
let mut ss = SemanticScholar::new();
let fields = vec![
    SsField::Title,
    SsField::Abstract,
];

let paper_details: SsResponse = ss.query_paper_details(paper_id.to_string(), fields).await;

rs-ss-tools → クレート / GitHub

中心になる構造体はarXivのときと似た構造になっています.

pub struct SemanticScholar {
    pub api_key: String,
    pub base_url: String,
    pub endpoint: SsEndpoint,
    pub query_text: String,
    pub fields: Vec<SsField>,
}

ベースのURLはhttps://api.semanticscholar.org/graph/v1/となっており,これは固定です.
arXivのときと大きく異なるのは,api_keyendpointがある点です.

Semantich Scholarでは,高速なレスポンスが必要なユーザー向けにAPIキーを発行しています.
https://www.semanticscholar.org/product/api

API自体はAPIキーがなくても使えるので,ss-toolsもAPIキーがあればオプションで読み込むようにします.

APIキーを使用するには,reqwestを使ってリクエストのヘッダーにx-api-keyを追加します.

let mut headers = header::HeaderMap::new();
if !self.api_key.is_empty() {          
    headers.insert("x-api-key", self.api_key.parse().unwrap());
}
let client = request::Client::builder()
    .default_headers(headers)
    .build()
    .unwrap();

let url = self.build();
let body = client.get(url).send().await.unwrap().text().await.unwrap();

なお,APIキー自体は環境変数から取得するようにしています.

arXivでは,クエリの設定を関数でチェインするように作っていましたが,SemanticScholarではエンドポイントが異なり,かつ設定する項目はほとんどfieldsなので,チェイン形式ではなくエンドポイントごとに関数を作る仕様にしました.

impl SemanticScholar {
    pub fn new() -> Self {
        dotenv().ok();
        let vars = FxHashMap::from_iter(std::env::vars().into_iter().map(|(k, v)| (k, v)));
        let api_key = vars
            .get("SEMANTIC_SCHOLAR_API_KEY")
            .unwrap_or(&"".to_string())
            .to_string();
        Self {
            api_key: api_key,
            base_url: "https://api.semanticscholar.org/graph/v1/".to_string(),
            endpoint: SsEndpoint::GetPaperTitle,
            query_text: "".to_string(),
            fields: vec![],
        }
    }

    fn build(&self) -> String {
        match &self.endpoint {
            SsEndpoint::GetPaperTitle => {
                let url = format!(
                    "{}paper/search/match?query={}",
                    self.base_url, self.query_text
                );
                return url;
            }
            SsEndpoint::GetPaperDetails => {
                let fields = self
                    .fields
                    .iter()
                    .map(|field| field.to_string())
                    .collect::<Vec<String>>()
                    .join(",");
                let url = format!(
                    "{}paper/{}?fields={}",
                    self.base_url, self.query_text, fields
                );
                return url;
            }
            SsEndpoint::GetAuthorDetails => {
                let fields = self
                    .fields
                    .iter()
                    .map(|field| field.to_string())
                    .collect::<Vec<String>>()
                    .join(",");
                let url = format!(
                    "{}author/{}?fields={}",
                    self.base_url, self.query_text, fields
                );
                return url;
            }
            SsEndpoint::GetReferencesOfAPaper(paper_id) => {
                // TODO: Implement
                return "".to_string();
            }
            SsEndpoint::GetCitationsOfAPaper(paper_id) => {
                // TODO: Implement
                return "".to_string();
            }
        }
    }
    pub async fn query_paper_id(&mut self, query_text: String) -> String {
        self.query_text = query_text;
        self.endpoint = SsEndpoint::GetPaperTitle;

        let mut headers = header::HeaderMap::new();
        if !self.api_key.is_empty() {
            headers.insert("x-api-key", self.api_key.parse().unwrap());
        }
        let client = request::Client::builder()
            .default_headers(headers)
            .build()
            .unwrap();

        let url = self.build();
        let body = client.get(url).send().await.unwrap().text().await.unwrap();
        let response = serde_json::from_str::<SsResponsePpaerIds>(&body).unwrap();

        // TODO: rerank paper ids based on the similarity of the query text
        let paper_id = response.data[0].paper_id.clone();

        return paper_id.unwrap();
    }

    pub async fn query_paper_details(
        &mut self,
        paper_id: String,
        fields: Vec<SsField>,
    ) -> SsResponse {
        self.query_text = paper_id;
        self.fields = fields.clone();
        self.endpoint = SsEndpoint::GetPaperDetails;

        if !fields.contains(&SsField::PaperId) {
            self.fields.push(SsField::PaperId);
        }

        let mut headers = header::HeaderMap::new();
        if !self.api_key.is_empty() {
            headers.insert("x-api-key", self.api_key.parse().unwrap());
        }
        let client = request::Client::builder()
            .default_headers(headers)
            .build()
            .unwrap();

        let url = self.build();
        let body = client.get(url).send().await.unwrap().text().await.unwrap();
        println!("{}", body);
        let response = serde_json::from_str::<SsResponse>(&body).unwrap();

        return response;
    }
}

Semantic Scholarでは,論文に関する欲しい情報をfieldsというパラメータで指定する仕様になっています.
そのため,これを簡単に扱える構造体があると便利です. 
enumで実装してみました.
Authorsなど一部のフィールドはさらに詳細を設定することができるようになっているので,それぞれの詳細パラメータは別のenumに定義しています.

#[derive(Debug, Clone, PartialEq)]
pub enum SsField {
    PaperId,
    Corpusid,
    Url,
    Title,
    Abstract,
    Venue,
    PublicationVenue,
    Year,
    ReferenceCount,
    CitationCount,
    InfluentialCitationCount,
    IsOpenAccess,
    OpenAccessPdf,
    FieldsOfStudy,
    S2FieldsOfStudy,
    PublicationTypes,
    PublicationDate,
    Journal,
    CitationStyles,
    Authors(Vec<SsAuthorField>),
    Citations(Vec<SsField>),
    References(Vec<SsField>),
    Embedding,
}

このSsFieldをリストで渡すイメージなのですが,最終的なfieldsのパラメータはfields=tilte,abstractのようにカンマ区切りになるので,これも簡単に扱えるように専用の関数を用意しておきます.

impl SsField {
    pub fn to_string(&self) -> String {
        match self {
            SsField::PaperId => "paperId".to_string(),
            SsField::Corpusid => "corpusId".to_string(),
            SsField::Url => "url".to_string(),
            SsField::Title => "title".to_string(),
            ...
        }
    }
}

これで,クエリパラメータを組み立てるときには,以下のようにSsFieldのリストをイテレートするだけで済むようになります.

let fields = self
    .fields
    .iter()
    .map(|field| field.to_string())
    .collect::<Vec<String>>()
    .join(",");

最後に,APIのレスポンスをパースする構造体を定義します.
こちらもarXivと同じ方針ですが,SemanticScholarのレスポンスはJSONなので,そのまま構造体にデシリアライズします.
面倒なXMLのパースは必要ないので,適切に構造体を設計してあげるだけでパースは簡単に処理できます.
ただし,Semantic ScholarのAPIはクエリパラメータに含まれていないフィールドは返してくれないので,ほとんどの項目はOption<T>になります.arXivのときは使用しませんでしたが,default_valueという関数を定義しておいて,レスポンスのJSONにフィールドが存在しなかった場合にNoneを返すようにしています.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SsResponse {
    #[serde(rename = "paperId", default = "default_value")]
    pub paper_id: Option<String>,
    #[serde(rename = "corpusId", default = "default_value")]
    pub corpus_id: Option<u32>,
    #[serde(default = "default_value")]
    pub url: Option<String>,
    #[serde(default = "default_value")]
    pub title: Option<String>,
    #[serde(rename = "abstract", default = "default_value")]
    pub abstract_text: Option<String>,
    #[serde(default = "default_value")]
    pub venue: Option<String>,
    #[serde(rename = "publicationVenue", default = "default_value")]
    pub publication_venue: Option<SsResponsePublicationVenue>,
    #[serde(default = "default_value")]
    pub year: Option<u32>,
    #[serde(rename = "referenceCount", default = "default_value")]
    pub reference_count: Option<u32>,
    #[serde(rename = "citationCount", default = "default_value")]
    pub citation_count: Option<u32>,
    #[serde(rename = "influentialCitationCount", default = "default_value")]
    pub influential_citation_count: Option<u32>,
    #[serde(rename = "isOpenAccess", default = "default_value")]
    pub is_open_access: Option<bool>,
    #[serde(rename = "openAccessPdf", default = "default_value")]
    pub open_access_pdf: Option<SsResponseOpenAccessPdf>,
    #[serde(rename = "fieldsOfStudy", default = "default_value")]
    pub fields_of_study: Option<Vec<String>>,
    #[serde(rename = "s2FieldsOfStudy", default = "default_value")]
    pub s2_fields_of_study: Option<Vec<SsResponseS2FieldsOfStudy>>,
    #[serde(rename = "publicationTypes", default = "default_value")]
    pub publication_types: Option<Vec<String>>,
    #[serde(rename = "publicationDate", default = "default_value")]
    pub publication_date: Option<String>,
    #[serde(default = "default_value")]
    pub journal: Option<SsResponseJournal>,
    #[serde(rename = "citationStyles", default = "default_value")]
    pub citation_styles: Option<SsResponseCitationStyles>,
    #[serde(default = "default_value")]
    pub authors: Option<Vec<SsResponseAuthor>>,
    #[serde(default = "default_value")]
    pub citations: Option<Vec<SsResponse>>,
    #[serde(default = "default_value")]
    pub references: Option<Vec<SsResponse>>,
    #[serde(default = "default_value")]
    pub embedding: Option<SsResponseEmbedding>,
    #[serde(rename = "matchScore", default = "default_value")]
    pub match_score: Option<f64>,
}

テスト結果がこちら.

et query_text = "attention is all you need";

let mut ss = SemanticScholar::new();
let paper_id = ss.query_paper_id(query_text.to_string()).await;

let mut ss = SemanticScholar::new();
let fields = vec![
    SsField::Title,
    SsField::Abstract,
    SsField::Authors(vec![
        SsAuthorField::Name,
        SsAuthorField::Affiliations,
        SsAuthorField::HIndex,
    ]),
    SsField::CitationCount,
    SsField::ReferenceCount,
    SsField::Year,
    SsField::IsOpenAccess,
    SsField::PublicationDate,
    SsField::Venue,
    SsField::FieldsOfStudy,
    SsField::Citations(vec![SsField::Title, SsField::Year, SsField::CitationCount]),
    SsField::References(vec![SsField::Title, SsField::Year, SsField::CitationCount]),
    SsField::Journal,
    SsField::PublicationVenue,
    SsField::OpenAccessPdf,
    SsField::S2FieldsOfStudy,
    SsField::PublicationTypes,
    SsField::CitationStyles,
    SsField::Embedding,
];

let paper_details: SsResponse = ss.query_paper_details(paper_id.to_string(), fields).await;
{
  "paperId": "204e3073870fae3d05bcbc2f6a8e263d9b72e776",
  "corpusId": null,
  "url": null,
  "title": "Attention is All you Need",
  "abstract": "The dominant sequence transduction models are based on complex recurrent or convolutional neural networks in an encoder-decoder configuration. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English-to-German translation task, improving over the existing best results, including ensembles by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data.",
  "venue": "Neural Information Processing Systems",
  "publicationVenue": {
    "id": "d9720b90-d60b-48bc-9df8-87a30b9a60dd",
    "name": "Neural Information Processing Systems",
    "type": "conference",
    "url": "http://neurips.cc/",
    "alternate_names": [
      "Neural Inf Process Syst",
      "NeurIPS",
      "NIPS"
    ]
  },
  "year": 2017,
  "referenceCount": 41,
  "citationCount": 111800,
  "influentialCitationCount": null,
  "isOpenAccess": false,
  "openAccessPdf": null,
  "fieldsOfStudy": [
    "Computer Science"
  ],
  "s2FieldsOfStudy": [
    {
      "category": "Computer Science",
      "source": "external"
    },
    {
      "category": "Computer Science",
      "source": "s2-fos-model"
    }
  ],
  "publicationTypes": [
    "JournalArticle",
    "Conference"
  ],
  "publicationDate": "2017-06-12",
  "journal": {
    "volume": null,
    "pages": "5998-6008",
    "name": null
  },
  "citationStyles": {
    "bibtex": "@Article{Vaswani2017AttentionIA,\n author = {Ashish Vaswani and Noam M. Shazeer and Niki Parmar and Jakob Uszkoreit and Llion Jones and Aidan N. Gomez and Lukasz Kaiser and Illia Polosukhin},\n booktitle = {Neural Information Processing Systems},\n pages = {5998-6008},\n title = {Attention is All you Need},\n year = {2017}\n}\n"
  },
...

毎度お世話になっている"Attention Is All You Need"ですが,欲しい情報は大体得られているようでした.

これで,arXivで論文のリストを取得し,SemanticScholarで引用情報などを補強して論文情報を整備することができるようになりました.

次回

次回はSemanticScholarのAPI実装で残っている課題を片付けます.

次回:Rustで学術論文からテキストを抽出する #16

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?