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}