pubmed_client/pubmed/client/
elink.rs

1//! ELink API operations for cross-referencing between NCBI databases
2
3use crate::error::Result;
4use crate::pubmed::models::{Citations, PmcLinks, RelatedArticles};
5use crate::pubmed::responses::ELinkResponse;
6use tracing::{debug, info, instrument};
7
8use super::PubMedClient;
9
10impl PubMedClient {
11    /// Get related articles for given PMIDs
12    ///
13    /// # Arguments
14    ///
15    /// * `pmids` - List of PubMed IDs to find related articles for
16    ///
17    /// # Returns
18    ///
19    /// Returns a `Result<RelatedArticles>` containing related article information
20    ///
21    /// # Example
22    ///
23    /// ```no_run
24    /// use pubmed_client::PubMedClient;
25    ///
26    /// #[tokio::main]
27    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28    ///     let client = PubMedClient::new();
29    ///     let related = client.get_related_articles(&[31978945]).await?;
30    ///     println!("Found {} related articles", related.related_pmids.len());
31    ///     Ok(())
32    /// }
33    /// ```
34    #[instrument(skip(self), fields(pmids_count = pmids.len()))]
35    pub async fn get_related_articles(&self, pmids: &[u32]) -> Result<RelatedArticles> {
36        if pmids.is_empty() {
37            return Ok(RelatedArticles {
38                source_pmids: Vec::new(),
39                related_pmids: Vec::new(),
40                link_type: "pubmed_pubmed".to_string(),
41            });
42        }
43
44        let elink_response = self.elink_request(pmids, "pubmed", "pubmed_pubmed").await?;
45
46        let mut all_related_pmids = Vec::new();
47
48        for linkset in elink_response.linksets {
49            if let Some(linkset_dbs) = linkset.linkset_dbs {
50                for linkset_db in linkset_dbs {
51                    if linkset_db.link_name == "pubmed_pubmed" {
52                        for link_id in linkset_db.links {
53                            if let Ok(pmid) = link_id.parse::<u32>() {
54                                all_related_pmids.push(pmid);
55                            }
56                        }
57                    }
58                }
59            }
60        }
61
62        // Remove duplicates and original PMIDs
63        all_related_pmids.sort_unstable();
64        all_related_pmids.dedup();
65        all_related_pmids.retain(|&pmid| !pmids.contains(&pmid));
66
67        info!(
68            source_count = pmids.len(),
69            related_count = all_related_pmids.len(),
70            "Related articles retrieved successfully"
71        );
72
73        Ok(RelatedArticles {
74            source_pmids: pmids.to_vec(),
75            related_pmids: all_related_pmids,
76            link_type: "pubmed_pubmed".to_string(),
77        })
78    }
79
80    /// Get PMC links for given PMIDs (full-text availability)
81    ///
82    /// # Arguments
83    ///
84    /// * `pmids` - List of PubMed IDs to check for PMC availability
85    ///
86    /// # Returns
87    ///
88    /// Returns a `Result<PmcLinks>` containing PMC IDs with full text available
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use pubmed_client::PubMedClient;
94    ///
95    /// #[tokio::main]
96    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
97    ///     let client = PubMedClient::new();
98    ///     let pmc_links = client.get_pmc_links(&[31978945]).await?;
99    ///     println!("Found {} PMC articles", pmc_links.pmc_ids.len());
100    ///     Ok(())
101    /// }
102    /// ```
103    #[instrument(skip(self), fields(pmids_count = pmids.len()))]
104    pub async fn get_pmc_links(&self, pmids: &[u32]) -> Result<PmcLinks> {
105        if pmids.is_empty() {
106            return Ok(PmcLinks {
107                source_pmids: Vec::new(),
108                pmc_ids: Vec::new(),
109            });
110        }
111
112        let elink_response = self.elink_request(pmids, "pmc", "pubmed_pmc").await?;
113
114        let mut pmc_ids = Vec::new();
115
116        for linkset in elink_response.linksets {
117            if let Some(linkset_dbs) = linkset.linkset_dbs {
118                for linkset_db in linkset_dbs {
119                    if linkset_db.link_name == "pubmed_pmc" && linkset_db.db_to == "pmc" {
120                        pmc_ids.extend(linkset_db.links);
121                    }
122                }
123            }
124        }
125
126        // Remove duplicates
127        pmc_ids.sort();
128        pmc_ids.dedup();
129
130        info!(
131            source_count = pmids.len(),
132            pmc_count = pmc_ids.len(),
133            "PMC links retrieved successfully"
134        );
135
136        Ok(PmcLinks {
137            source_pmids: pmids.to_vec(),
138            pmc_ids,
139        })
140    }
141
142    /// Get citing articles for given PMIDs
143    ///
144    /// This method retrieves articles that cite the specified PMIDs from the PubMed database.
145    /// The citation count returned represents only citations within the PubMed database
146    /// (peer-reviewed journal articles indexed in PubMed).
147    ///
148    /// # Important Note on Citation Counts
149    ///
150    /// The citation count from this method may be **lower** than counts from other sources like
151    /// Google Scholar, Web of Science, or scite.ai because:
152    ///
153    /// - **PubMed citations** (this method): Only includes peer-reviewed articles in PubMed
154    /// - **Google Scholar/scite.ai**: Includes preprints, books, conference proceedings, and other sources
155    ///
156    /// For example, PMID 31978945 shows:
157    /// - PubMed (this API): ~14,000 citations (PubMed database only)
158    /// - scite.ai: ~23,000 citations (broader sources)
159    ///
160    /// This is expected behavior - this method provides accurate PubMed-specific citation data.
161    ///
162    /// # Arguments
163    ///
164    /// * `pmids` - List of PubMed IDs to find citing articles for
165    ///
166    /// # Returns
167    ///
168    /// Returns a `Result<Citations>` containing citing article information
169    ///
170    /// # Example
171    ///
172    /// ```no_run
173    /// use pubmed_client::PubMedClient;
174    ///
175    /// #[tokio::main]
176    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
177    ///     let client = PubMedClient::new();
178    ///     let citations = client.get_citations(&[31978945]).await?;
179    ///     println!("Found {} citing articles in PubMed", citations.citing_pmids.len());
180    ///     Ok(())
181    /// }
182    /// ```
183    #[instrument(skip(self), fields(pmids_count = pmids.len()))]
184    pub async fn get_citations(&self, pmids: &[u32]) -> Result<Citations> {
185        if pmids.is_empty() {
186            return Ok(Citations {
187                source_pmids: Vec::new(),
188                citing_pmids: Vec::new(),
189                link_type: "pubmed_pubmed_citedin".to_string(),
190            });
191        }
192
193        let elink_response = self
194            .elink_request(pmids, "pubmed", "pubmed_pubmed_citedin")
195            .await?;
196
197        let mut citing_pmids = Vec::new();
198
199        for linkset in elink_response.linksets {
200            if let Some(linkset_dbs) = linkset.linkset_dbs {
201                for linkset_db in linkset_dbs {
202                    if linkset_db.link_name == "pubmed_pubmed_citedin" {
203                        for link_id in linkset_db.links {
204                            if let Ok(pmid) = link_id.parse::<u32>() {
205                                citing_pmids.push(pmid);
206                            }
207                        }
208                    }
209                }
210            }
211        }
212
213        // Remove duplicates
214        citing_pmids.sort_unstable();
215        citing_pmids.dedup();
216
217        info!(
218            source_count = pmids.len(),
219            citing_count = citing_pmids.len(),
220            "Citations retrieved successfully"
221        );
222
223        Ok(Citations {
224            source_pmids: pmids.to_vec(),
225            citing_pmids,
226            link_type: "pubmed_pubmed_citedin".to_string(),
227        })
228    }
229
230    /// Internal helper method for ELink API requests
231    pub(crate) async fn elink_request(
232        &self,
233        pmids: &[u32],
234        target_db: &str,
235        link_name: &str,
236    ) -> Result<ELinkResponse> {
237        // Convert PMIDs to strings and join with commas
238        let id_list: Vec<String> = pmids.iter().map(|id| id.to_string()).collect();
239        let ids = id_list.join(",");
240
241        // Build URL - API parameters will be added by make_request
242        let url = format!(
243            "{}/elink.fcgi?dbfrom=pubmed&db={}&id={}&linkname={}&retmode=json",
244            self.base_url,
245            urlencoding::encode(target_db),
246            urlencoding::encode(&ids),
247            urlencoding::encode(link_name)
248        );
249
250        debug!("Making ELink API request");
251        let response = self.make_request(&url).await?;
252
253        let elink_response: ELinkResponse = response.json().await?;
254        Ok(elink_response)
255    }
256}