Skip to main content

data_for_seo/api/content_analysis/
mod.rs

1use crate::entity::{
2    ContentAnalysisApiAvailableFilters, ContentAnalysisApiCategory,
3    ContentAnalysisApiCategoryTrends, ContentAnalysisApiIdList, ContentAnalysisApiLanguage,
4    ContentAnalysisApiLocation, ContentAnalysisApiPhraseTrends,
5    ContentAnalysisApiRatingDistribution, ContentAnalysisApiSearch,
6    ContentAnalysisApiSentimentAnalysis, ContentAnalysisApiSummary,
7};
8use crate::{DataForSeoApiResponse, DataForSeoClient};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Content Analysis API — citation search and sentiment analytics.
14/// See <https://docs.dataforseo.com/v3/content_analysis/overview/>.
15pub struct ContentAnalysisApi<'a> {
16    client: &'a DataForSeoClient,
17}
18
19impl DataForSeoClient {
20    /// Returns the Content Analysis API endpoints.
21    pub fn content_analysis(&self) -> ContentAnalysisApi<'_> {
22        ContentAnalysisApi { client: self }
23    }
24}
25
26impl ContentAnalysisApi<'_> {
27    /// Finds citations for the target keyword.
28    /// <https://docs.dataforseo.com/v3/content_analysis/search/live/>
29    pub async fn search(
30        &self,
31        data: Vec<ContentAnalysisApiSearchPost>,
32    ) -> DataForSeoApiResponse<ContentAnalysisApiSearch> {
33        self.client
34            .http_post("/v3/content_analysis/search/live", &data)
35            .await
36    }
37
38    /// Overview of the citation data available for the target keyword.
39    /// <https://docs.dataforseo.com/v3/content_analysis/summary/live/>
40    pub async fn summary(
41        &self,
42        data: Vec<ContentAnalysisApiSummaryPost>,
43    ) -> DataForSeoApiResponse<ContentAnalysisApiSummary> {
44        self.client
45            .http_post("/v3/content_analysis/summary/live", &data)
46            .await
47    }
48
49    /// Citation stats by sentiment polarity and connotation.
50    /// <https://docs.dataforseo.com/v3/content_analysis/sentiment_analysis/live/>
51    pub async fn sentiment_analysis(
52        &self,
53        data: Vec<ContentAnalysisApiSentimentAnalysisPost>,
54    ) -> DataForSeoApiResponse<ContentAnalysisApiSentimentAnalysis> {
55        self.client
56            .http_post("/v3/content_analysis/sentiment_analysis/live", &data)
57            .await
58    }
59
60    /// Citation stats distributed by content rating.
61    /// <https://docs.dataforseo.com/v3/content_analysis/rating_distribution/live/>
62    pub async fn rating_distribution(
63        &self,
64        data: Vec<ContentAnalysisApiRatingDistributionPost>,
65    ) -> DataForSeoApiResponse<ContentAnalysisApiRatingDistribution> {
66        self.client
67            .http_post("/v3/content_analysis/rating_distribution/live", &data)
68            .await
69    }
70
71    /// Citation stats by date for the target keyword.
72    /// <https://docs.dataforseo.com/v3/content_analysis/phrase_trends/live/>
73    pub async fn phrase_trends(
74        &self,
75        data: Vec<ContentAnalysisApiPhraseTrendsPost>,
76    ) -> DataForSeoApiResponse<ContentAnalysisApiPhraseTrends> {
77        self.client
78            .http_post("/v3/content_analysis/phrase_trends/live", &data)
79            .await
80    }
81
82    /// Citation stats by date for the target category.
83    /// <https://docs.dataforseo.com/v3/content_analysis/category_trends/live/>
84    pub async fn category_trends(
85        &self,
86        data: Vec<ContentAnalysisApiCategoryTrendsPost>,
87    ) -> DataForSeoApiResponse<ContentAnalysisApiCategoryTrends> {
88        self.client
89            .http_post("/v3/content_analysis/category_trends/live", &data)
90            .await
91    }
92
93    /// Lists tasks set within the specified datetime range.
94    /// <https://docs.dataforseo.com/v3/content_analysis/id_list/>
95    pub async fn id_list(
96        &self,
97        data: Vec<ContentAnalysisApiIdListPost>,
98    ) -> DataForSeoApiResponse<ContentAnalysisApiIdList> {
99        self.client
100            .http_post("/v3/content_analysis/id_list", &data)
101            .await
102    }
103
104    /// Categories available in the content-analysis taxonomy.
105    /// <https://docs.dataforseo.com/v3/content_analysis/categories/>
106    pub async fn categories(&self) -> DataForSeoApiResponse<ContentAnalysisApiCategory> {
107        self.client
108            .http_get("/v3/content_analysis/categories")
109            .await
110    }
111
112    /// Locations supported by the Content Analysis API.
113    /// <https://docs.dataforseo.com/v3/content_analysis/locations/>
114    pub async fn locations(&self) -> DataForSeoApiResponse<ContentAnalysisApiLocation> {
115        self.client.http_get("/v3/content_analysis/locations").await
116    }
117
118    /// Languages supported by the Content Analysis API.
119    /// <https://docs.dataforseo.com/v3/content_analysis/languages/>
120    pub async fn languages(&self) -> DataForSeoApiResponse<ContentAnalysisApiLanguage> {
121        self.client.http_get("/v3/content_analysis/languages").await
122    }
123
124    /// Filters that can be used in `filters` / `initial_dataset_filters`.
125    /// <https://docs.dataforseo.com/v3/content_analysis/available_filters/>
126    pub async fn available_filters(
127        &self,
128    ) -> DataForSeoApiResponse<ContentAnalysisApiAvailableFilters> {
129        self.client
130            .http_get("/v3/content_analysis/available_filters")
131            .await
132    }
133}
134
135/// Request for `content_analysis/search/live`.
136#[derive(Debug, Default, Serialize, Deserialize, Clone)]
137pub struct ContentAnalysisApiSearchPost {
138    /// Target keyword (UTF-8, lowercased); wrap in escaped double quotes for an
139    /// exact phrase.
140    pub keyword: String,
141    /// Restrict matches to keywords appearing in given fields (title,
142    /// main_title, previous_title, snippet).
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub keyword_fields: Option<HashMap<String, String>>,
145    /// Filter by page types (ecommerce, news, blogs, message-boards,
146    /// organization).
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub page_type: Option<Vec<String>>,
149    /// Results grouping: `as_is` (default) or `one_per_domain`.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub search_mode: Option<String>,
152    /// Maximum citations to return (default 100, max 1000).
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub limit: Option<i32>,
155    /// Up to 8 filtering conditions joined by `and`/`or`.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub filters: Option<Vec<Value>>,
158    /// Up to 3 sorting rules, e.g. `content_info.sentiment_connotations.anger,desc`.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub order_by: Option<Vec<String>>,
161    /// Number of leading citations to skip; use for up to 10,000 results.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub offset: Option<i32>,
164    /// Token from a previous response to page beyond 10,000 results.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub offset_token: Option<String>,
167    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub rank_scale: Option<String>,
170    /// User-defined task identifier (max 255 characters).
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub tag: Option<String>,
173}
174
175impl ContentAnalysisApiSearchPost {
176    /// Creates a request for the given target keyword.
177    pub fn new(keyword: &str) -> ContentAnalysisApiSearchPost {
178        ContentAnalysisApiSearchPost {
179            keyword: keyword.to_string(),
180            ..Default::default()
181        }
182    }
183}
184
185/// Request for `content_analysis/summary/live`.
186#[derive(Debug, Default, Serialize, Deserialize, Clone)]
187pub struct ContentAnalysisApiSummaryPost {
188    /// Target keyword (UTF-8, lowercased); wrap in escaped double quotes for an
189    /// exact phrase.
190    pub keyword: String,
191    /// Restrict matches to keywords appearing in given fields (title,
192    /// main_title, previous_title, snippet).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub keyword_fields: Option<HashMap<String, String>>,
195    /// Filter by page types (ecommerce, news, blogs, message-boards,
196    /// organization).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub page_type: Option<Vec<String>>,
199    /// Maximum elements within internal arrays like top_domains and countries
200    /// (default 1, max 20).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub internal_list_limit: Option<i32>,
203    /// Minimum positive-sentiment probability for a citation to count
204    /// (0–1, default 0.4).
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub positive_connotation_threshold: Option<f64>,
207    /// Minimum per-sentiment probability for a citation to count
208    /// (0–1, default 0.4).
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub sentiments_connotation_threshold: Option<f64>,
211    /// Up to 8 filtering conditions applied to Search fields before aggregation.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub initial_dataset_filters: Option<Vec<Value>>,
214    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub rank_scale: Option<String>,
217    /// User-defined task identifier (max 255 characters).
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub tag: Option<String>,
220}
221
222impl ContentAnalysisApiSummaryPost {
223    /// Creates a request for the given target keyword.
224    pub fn new(keyword: &str) -> ContentAnalysisApiSummaryPost {
225        ContentAnalysisApiSummaryPost {
226            keyword: keyword.to_string(),
227            ..Default::default()
228        }
229    }
230}
231
232/// Request for `content_analysis/sentiment_analysis/live`.
233#[derive(Debug, Default, Serialize, Deserialize, Clone)]
234pub struct ContentAnalysisApiSentimentAnalysisPost {
235    /// Target keyword (UTF-8, lowercased); wrap in escaped double quotes for an
236    /// exact phrase.
237    pub keyword: String,
238    /// Restrict matches to keywords appearing in given fields (title,
239    /// main_title, previous_title, snippet).
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub keyword_fields: Option<HashMap<String, String>>,
242    /// Filter by page types (ecommerce, news, blogs, message-boards,
243    /// organization).
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub page_type: Option<Vec<String>>,
246    /// Maximum elements within internal arrays like top_domains and countries
247    /// (default 1, max 20).
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub internal_list_limit: Option<i32>,
250    /// Minimum positive-sentiment probability for a citation to count
251    /// (0–1, default 0.4).
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub positive_connotation_threshold: Option<f64>,
254    /// Minimum per-sentiment probability for a citation to count
255    /// (0–1, default 0.4).
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub sentiments_connotation_threshold: Option<f64>,
258    /// Up to 8 filtering conditions applied to Search fields before aggregation.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub initial_dataset_filters: Option<Vec<Value>>,
261    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub rank_scale: Option<String>,
264    /// User-defined task identifier (max 255 characters).
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub tag: Option<String>,
267}
268
269impl ContentAnalysisApiSentimentAnalysisPost {
270    /// Creates a request for the given target keyword.
271    pub fn new(keyword: &str) -> ContentAnalysisApiSentimentAnalysisPost {
272        ContentAnalysisApiSentimentAnalysisPost {
273            keyword: keyword.to_string(),
274            ..Default::default()
275        }
276    }
277}
278
279/// Request for `content_analysis/rating_distribution/live`.
280#[derive(Debug, Default, Serialize, Deserialize, Clone)]
281pub struct ContentAnalysisApiRatingDistributionPost {
282    /// Target keyword (UTF-8, lowercased); wrap in escaped double quotes for an
283    /// exact phrase.
284    pub keyword: String,
285    /// Restrict matches to keywords appearing in given fields (title,
286    /// main_title, previous_title, snippet).
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub keyword_fields: Option<HashMap<String, String>>,
289    /// Filter by page types (ecommerce, news, blogs, message-boards,
290    /// organization).
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub page_type: Option<Vec<String>>,
293    /// Maximum elements within internal arrays like top_domains and countries
294    /// (default 1, max 20).
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub internal_list_limit: Option<i32>,
297    /// Results grouping: `as_is` (default) or `one_per_domain`.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub search_mode: Option<String>,
300    /// Minimum positive-sentiment probability for a citation to count
301    /// (0–1, default 0.4).
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub positive_connotation_threshold: Option<f64>,
304    /// Minimum per-sentiment probability for a citation to count
305    /// (0–1, default 0.4).
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub sentiments_connotation_threshold: Option<f64>,
308    /// Up to 8 filtering conditions applied to Search fields before aggregation.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub initial_dataset_filters: Option<Vec<Value>>,
311    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub rank_scale: Option<String>,
314    /// User-defined task identifier (max 255 characters).
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub tag: Option<String>,
317}
318
319impl ContentAnalysisApiRatingDistributionPost {
320    /// Creates a request for the given target keyword.
321    pub fn new(keyword: &str) -> ContentAnalysisApiRatingDistributionPost {
322        ContentAnalysisApiRatingDistributionPost {
323            keyword: keyword.to_string(),
324            ..Default::default()
325        }
326    }
327}
328
329/// Request for `content_analysis/phrase_trends/live`.
330#[derive(Debug, Default, Serialize, Deserialize, Clone)]
331pub struct ContentAnalysisApiPhraseTrendsPost {
332    /// Target keyword (UTF-8, lowercased); wrap in escaped double quotes for an
333    /// exact phrase.
334    pub keyword: String,
335    /// Start of the date range, `yyyy-mm-dd`.
336    pub date_from: String,
337    /// End of the date range, `yyyy-mm-dd` (defaults to today).
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub date_to: Option<String>,
340    /// Grouping interval: `day`, `week`, or `month` (default).
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub date_group: Option<String>,
343    /// Restrict matches to keywords appearing in given fields (title,
344    /// main_title, previous_title, snippet).
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub keyword_fields: Option<HashMap<String, String>>,
347    /// Filter by page types (ecommerce, news, blogs, message-boards,
348    /// organization).
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub page_type: Option<Vec<String>>,
351    /// Results grouping: `as_is` (default) or `one_per_domain`.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub search_mode: Option<String>,
354    /// Maximum elements within internal arrays like top_domains and countries
355    /// (default 1, max 20).
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub internal_list_limit: Option<i32>,
358    /// Up to 8 filtering conditions applied to Search fields before aggregation.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub initial_dataset_filters: Option<Vec<Value>>,
361    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub rank_scale: Option<String>,
364    /// User-defined task identifier (max 255 characters).
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub tag: Option<String>,
367}
368
369impl ContentAnalysisApiPhraseTrendsPost {
370    /// Creates a request for the given keyword and start date (`yyyy-mm-dd`).
371    pub fn new(keyword: &str, date_from: &str) -> ContentAnalysisApiPhraseTrendsPost {
372        ContentAnalysisApiPhraseTrendsPost {
373            keyword: keyword.to_string(),
374            date_from: date_from.to_string(),
375            ..Default::default()
376        }
377    }
378}
379
380/// Request for `content_analysis/category_trends/live`.
381#[derive(Debug, Default, Serialize, Deserialize, Clone)]
382pub struct ContentAnalysisApiCategoryTrendsPost {
383    /// Target category code; obtain it from the Categories endpoint.
384    pub category_code: i64,
385    /// Start of the date range, `yyyy-mm-dd` (minimum 2022-10-31).
386    pub date_from: String,
387    /// End of the date range, `yyyy-mm-dd` (defaults to today).
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub date_to: Option<String>,
390    /// Grouping interval: `day`, `week`, or `month` (default).
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub date_group: Option<String>,
393    /// Filter by page types (ecommerce, news, blogs, message-boards,
394    /// organization).
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub page_type: Option<Vec<String>>,
397    /// Results grouping: `as_is` (default) or `one_per_domain`.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub search_mode: Option<String>,
400    /// Maximum elements within internal arrays like top_domains and countries
401    /// (default 1, max 20).
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub internal_list_limit: Option<i32>,
404    /// Up to 8 filtering conditions applied to Search fields before aggregation.
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub initial_dataset_filters: Option<Vec<Value>>,
407    /// Scale for rank values: `one_hundred` or `one_thousand` (default).
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub rank_scale: Option<String>,
410    /// User-defined task identifier (max 255 characters).
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub tag: Option<String>,
413}
414
415impl ContentAnalysisApiCategoryTrendsPost {
416    /// Creates a request for the given category code and start date (`yyyy-mm-dd`).
417    pub fn new(category_code: i64, date_from: &str) -> ContentAnalysisApiCategoryTrendsPost {
418        ContentAnalysisApiCategoryTrendsPost {
419            category_code,
420            date_from: date_from.to_string(),
421            ..Default::default()
422        }
423    }
424}
425
426/// Request for `content_analysis/id_list`.
427#[derive(Debug, Default, Serialize, Deserialize, Clone)]
428pub struct ContentAnalysisApiIdListPost {
429    /// Start of the range, UTC `yyyy-mm-dd hh-mm-ss +00:00`.
430    pub datetime_from: String,
431    /// End of the range, UTC `yyyy-mm-dd hh-mm-ss +00:00`.
432    pub datetime_to: String,
433    /// Maximum task IDs to return (default 1000, max 1000).
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub limit: Option<i32>,
436    /// Number of leading task IDs to skip (default 0).
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub offset: Option<i32>,
439    /// Sort by task execution time: `asc` (default) or `desc`.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub sort: Option<String>,
442    /// Include each task's request metadata (default false).
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub include_metadata: Option<bool>,
445}
446
447impl ContentAnalysisApiIdListPost {
448    /// Creates a request for the given datetime range
449    /// (`yyyy-mm-dd hh-mm-ss +00:00`).
450    pub fn new(datetime_from: &str, datetime_to: &str) -> ContentAnalysisApiIdListPost {
451        ContentAnalysisApiIdListPost {
452            datetime_from: datetime_from.to_string(),
453            datetime_to: datetime_to.to_string(),
454            ..Default::default()
455        }
456    }
457}