File Coverage

blib/lib/Astro/ADS/Search.pm
Criterion Covered Total %
statement 81 95 85.2
branch 18 28 64.2
condition 3 3 100.0
subroutine 14 16 87.5
pod 5 6 83.3
total 121 148 81.7


line stmt bran cond sub pod time code
1             package Astro::ADS::Search;
2             $Astro::ADS::Search::VERSION = '1.92';
3 4     4   1661649 use Moo;
  4         35582  
  4         27  
4             extends 'Astro::ADS';
5             with 'Astro::ADS::Role::ResultMapper';
6              
7 4     4   6464 use Carp;
  4         11  
  4         359  
8 4     4   1664 use Data::Dumper::Concise;
  4         27310  
  4         447  
9 4     4   1215 use Mojo::Base -strict; # do we want -signatures
  4         25677  
  4         36  
10 4     4   3955 use Mojo::DOM;
  4         790900  
  4         232  
11 4     4   2006 use Mojo::File qw( path );
  4         188202  
  4         374  
12 4     4   2059 use Mojo::URL;
  4         39104  
  4         33  
13 4     4   214 use Mojo::Util qw( quote );
  4         10  
  4         283  
14 4     4   1617 use PerlX::Maybe;
  4         15406  
  4         24  
15 4     4   2498 use Types::Standard qw( Int Str ArrayRef HashRef ); # InstanceOf ConsumerOf
  4         704757  
  4         67  
16              
17             has [qw/q fq fl sort/] => (
18             is => 'rw',
19             );
20             has [qw/start rows/] => (
21             is => 'rw',
22             isa => Int->where( '$_ >= 0' ),
23             );
24             # TODO: add year and other fields
25             has [qw/authors objects bibcode/] => (
26             is => 'rw',
27             isa => ArrayRef[Str],
28             default => sub { return [] },
29             );
30             has [qw/author_logic object_logic/] => (
31             is => 'rw',
32             isa => HashRef[],
33             default => sub { return {} },
34             );
35              
36             before sort => sub {
37             my $orig = shift;
38             my $self = shift;
39             my ($field, $direction) = @_;
40              
41             return unless $field;
42              
43             my $sort_field_re = qr/(?:id|author_count|bibcode|citation_count|citation_count_norm|classic_factor|first_author|date|entry_date|read_count|score)/;
44             if ($field =~ /^$sort_field_re\+(?:asc|desc)$/) {
45             $orig->($self, $field);
46             }
47             if ($field !~ /^$sort_field_re$/) {
48             carp 'Invalid sort field: ', $field;
49             return;
50             }
51             if ($direction eq 'asc' || $direction eq 'desc') {
52             carp 'Invalid sort direction: ', $direction;
53             return;
54             }
55             $orig->($self, join('+', $field, $direction) );
56             };
57              
58             sub query {
59 6     6 1 44873 my ($self, $terms) = @_;
60 6         114 my $url = $self->base_url->clone->path('search/query');
61 6 50       1957 my $search_terms = $self->gather_search_terms( $terms ) or return;
62              
63 6         36 $url->query( $search_terms );
64 6         1202 my $response = $self->get_response( $url );
65 6 100       23 if ( $response->is_error ) {
66 1         31 carp $response->message;
67 1         187 my $error_obj = {
68             message => $response->message,
69             query => $search_terms,
70             url => $url,
71             };
72 1         23 return Astro::ADS::Result->new( {error => $error_obj} );
73             }
74              
75 5         109 my $json = $response->json;
76 5         497 return $self->parse_response( $json );
77             }
78              
79             sub query_tree {
80 1     1 1 6911 my ($self, $terms) = @_;
81              
82 1         15 my $url = $self->base_url->clone->path('search/qtree');
83 1         353 $url->query( { q => $self->q, fl => $self->fl } );
84 1         177 my $response = $self->get_response( $url );
85              
86 1         10 my $qtree = $self->parse_qtree_response( $response );
87 1         46764 return $qtree;
88             }
89              
90             sub bigquery {
91 1     1 1 3796 my ($self, @bibcodes) = @_;
92              
93 1         4 my $bibcode_list = join "\n", 'bibcode', @bibcodes;
94              
95 1         12 my $url = $self->base_url->clone->path('search/bigquery');
96             #TODO why the parameters in the query?
97 1         320 $url->query( { q => $self->q, fl => $self->fl } );
98 1         184 my $response = $self->post_response( $url, $bibcode_list );
99              
100 1         4 my $json = $response->json;
101 1         78 return $self->parse_response( $json );
102             }
103              
104             sub gather_search_terms {
105 8     8 0 8817 my ($self, $terms) = @_;
106              
107 8         22 my @query = ();
108 8 100 100     62 if ( $terms && $terms->{q} ) {
109 2         9 push @query, delete $terms->{q};
110             }
111             else {
112 6 50       65 push @query, $self->q if $self->q;
113 6 100       31 push @query, delete $terms->{'+q'} if exists $terms->{'+q'};
114              
115 6 100       11 if ( @{$self->authors} ) {
  6         235  
116 1         12 my $tag = 'author:';
117 1 50       31 substr($tag, 0, 0) = '=' if $self->author_logic->{exact};
118 1 50       13 if ( @{$self->authors} > 1 ) {
  1         27  
119 1 50       33 my $logic = $self->author_logic->{OR} ? q{ OR } : q{ };
120 1         43 push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->authors}) . ')';
  2         25  
  1         28  
121             }
122             else {
123 0         0 push @query, $tag . quote $self->authors->[0];
124             }
125             }
126              
127 6 100       69 if ( @{$self->objects} ) {
  6         143  
128 1         11 my $tag = 'object:';
129 1 50       2 if ( @{$self->objects} > 1 ) {
  1         23  
130 0 0       0 my $logic = $self->object_logic->{OR} ? q{ OR } : q{ };
131 0         0 push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->objects}) . ')';
  0         0  
  0         0  
132             }
133             else {
134 1         37 push @query, $tag . quote $self->objects->[0];
135             }
136             }
137              
138             # need to remember which attributes take multiple values
139 6 50       64 push @query, @{$self->bibcode} if @{$self->bibcode};
  0         0  
  6         175  
140             }
141            
142 8 50       74 unless ( @query ) {
143 0         0 carp 'No search terms provided for query';
144 0         0 return;
145             }
146              
147 8         258 my $search_terms = {
148             q => join(q{ }, @query),
149             maybe fq => $self->fq,
150             maybe fl => $self->fl,
151             maybe start => $self->start,
152             maybe rows => $self->rows,
153             maybe sort => $self->sort,
154             %$terms
155             };
156              
157 8         152 return $search_terms;
158             }
159              
160             sub add_authors {
161 0     0 1   my ($self, @authors) = @_;
162 0           push @{$self->authors}, @authors;
  0            
163             }
164              
165             sub add_objects {
166 0     0 1   my ($self, @objects) = @_;
167 0           push @{$self->objects}, @objects;
  0            
168             }
169              
170             1;
171              
172             =pod
173              
174             =encoding UTF-8
175              
176             =head1 NAME
177              
178             Astro::ADS::Search - Queries the ADS Search endpoint and collects the results
179              
180             =head1 VERSION
181              
182             version 1.92
183              
184             =head1 SYNOPSIS
185              
186             my $search = Astro::ADS::Search->new({
187             q => '...', # initial search query
188             fl => '...', # return list of attributes
189             });
190              
191             my $result = $search->query();
192             my @papers = $result->papers();
193              
194             while ( my $t = $result->next_query() ) {
195             $result = $search->query( $t );
196             push @papers, $result->get_papers();
197             }
198              
199             while ( my $t = $result->next_query() ) {
200             push @papers, $result->more_papers( $t );
201             }
202              
203             while ( push @papers, $result->next_query()->more_papers() ) {
204             }
205              
206             =head1 DESCRIPTION
207              
208             Search for papers in the Harvard ADS
209              
210             You can put base terms in the creation of the object and use the
211             query method to add new terms to that query only
212              
213             =head1 Methods
214              
215             =head2 query
216              
217             Adding a field key C<+q> to the query method B<adds> the query
218             term to the existing query terms,
219             whereas specifying a value for C<q> in the query method
220             overwrites the query terms and neglects gathering other search attributes,
221             such as authors or objects.
222              
223             =head2 add_authors
224              
225             Add a list of authors to a search query. Authors added here will not be
226             deleted if the query attribute is updated.
227              
228             =head2 add_objects
229              
230             Add a list of objects to a search query. Objects added here will not be
231             deleted if the query attribute is updated.
232              
233             =head2 query_tree
234              
235             Will return the L<Abstract Syntax Tree|https://ui.adsabs.harvard.edu/help/api/api-docs.html#get-/search/qtree> for the query.
236              
237             =head2 bigquery
238              
239             Accepts a L<list of many IDs|https://ui.adsabs.harvard.edu/help/api/api-docs.html#post-/search/bigquery> and supports paging.
240              
241             =head2 Notes
242              
243             From the ADS API, the "=" sign turns off the synonym expansion feature
244             available with the author and title fields
245              
246             =head1 See Also
247              
248             =over 4
249              
250             =item * L<Astro::ADS>
251              
252             =item * L<Astro::ADS::Result>
253              
254             =item * L<ADS API|https://ui.adsabs.harvard.edu/help/api/>
255              
256             =item * L<Search Syntax|https://ui.adsabs.harvard.edu/help/search/search-syntax>
257              
258             =back
259              
260             =head1 COPYRIGHT AND LICENSE
261              
262             This software is Copyright (c) 2025 by Boyd Duffee.
263              
264             This is free software, licensed under:
265              
266             The MIT (X11) License
267              
268             =cut