Astro-ADS/lib/Astro/ADS/Search.pm
package Astro::ADS::Search;
$Astro::ADS::Search::VERSION = '1.92';
use Moo;
extends 'Astro::ADS';
with 'Astro::ADS::Role::ResultMapper';
use Carp;
use Data::Dumper::Concise;
use Mojo::Base -strict; # do we want -signatures
use Mojo::DOM;
use Mojo::File qw( path );
use Mojo::URL;
use Mojo::Util qw( quote );
use PerlX::Maybe;
use Types::Standard qw( Int Str ArrayRef HashRef ); # InstanceOf ConsumerOf
has [qw/q fq fl sort/] => (
is => 'rw',
);
has [qw/start rows/] => (
is => 'rw',
isa => Int->where( '$_ >= 0' ),
);
# TODO: add year and other fields
has [qw/authors objects bibcode/] => (
is => 'rw',
isa => ArrayRef[Str],
default => sub { return [] },
);
has [qw/author_logic object_logic/] => (
is => 'rw',
isa => HashRef[],
default => sub { return {} },
);
before sort => sub {
my $orig = shift;
my $self = shift;
my ($field, $direction) = @_;
return unless $field;
my $sort_field_re = qr/(?:id|author_count|bibcode|citation_count|citation_count_norm|classic_factor|first_author|date|entry_date|read_count|score)/;
if ($field =~ /^$sort_field_re\+(?:asc|desc)$/) {
$orig->($self, $field);
}
if ($field !~ /^$sort_field_re$/) {
carp 'Invalid sort field: ', $field;
return;
}
if ($direction eq 'asc' || $direction eq 'desc') {
carp 'Invalid sort direction: ', $direction;
return;
}
$orig->($self, join('+', $field, $direction) );
};
sub query {
my ($self, $terms) = @_;
my $url = $self->base_url->clone->path('search/query');
my $search_terms = $self->gather_search_terms( $terms ) or return;
$url->query( $search_terms );
my $response = $self->get_response( $url );
if ( $response->is_error ) {
carp $response->message;
my $error_obj = {
message => $response->message,
query => $search_terms,
url => $url,
};
return Astro::ADS::Result->new( {error => $error_obj} );
}
my $json = $response->json;
return $self->parse_response( $json );
}
sub query_tree {
my ($self, $terms) = @_;
my $url = $self->base_url->clone->path('search/qtree');
$url->query( { q => $self->q, fl => $self->fl } );
my $response = $self->get_response( $url );
my $qtree = $self->parse_qtree_response( $response );
return $qtree;
}
sub bigquery {
my ($self, @bibcodes) = @_;
my $bibcode_list = join "\n", 'bibcode', @bibcodes;
my $url = $self->base_url->clone->path('search/bigquery');
#TODO why the parameters in the query?
$url->query( { q => $self->q, fl => $self->fl } );
my $response = $self->post_response( $url, $bibcode_list );
my $json = $response->json;
return $self->parse_response( $json );
}
sub gather_search_terms {
my ($self, $terms) = @_;
my @query = ();
if ( $terms && $terms->{q} ) {
push @query, delete $terms->{q};
}
else {
push @query, $self->q if $self->q;
push @query, delete $terms->{'+q'} if exists $terms->{'+q'};
if ( @{$self->authors} ) {
my $tag = 'author:';
substr($tag, 0, 0) = '=' if $self->author_logic->{exact};
if ( @{$self->authors} > 1 ) {
my $logic = $self->author_logic->{OR} ? q{ OR } : q{ };
push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->authors}) . ')';
}
else {
push @query, $tag . quote $self->authors->[0];
}
}
if ( @{$self->objects} ) {
my $tag = 'object:';
if ( @{$self->objects} > 1 ) {
my $logic = $self->object_logic->{OR} ? q{ OR } : q{ };
push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->objects}) . ')';
}
else {
push @query, $tag . quote $self->objects->[0];
}
}
# need to remember which attributes take multiple values
push @query, @{$self->bibcode} if @{$self->bibcode};
}
unless ( @query ) {
carp 'No search terms provided for query';
return;
}
my $search_terms = {
q => join(q{ }, @query),
maybe fq => $self->fq,
maybe fl => $self->fl,
maybe start => $self->start,
maybe rows => $self->rows,
maybe sort => $self->sort,
%$terms
};
return $search_terms;
}
sub add_authors {
my ($self, @authors) = @_;
push @{$self->authors}, @authors;
}
sub add_objects {
my ($self, @objects) = @_;
push @{$self->objects}, @objects;
}
1;
=pod
=encoding UTF-8
=head1 NAME
Astro::ADS::Search - Queries the ADS Search endpoint and collects the results
=head1 VERSION
version 1.92
=head1 SYNOPSIS
my $search = Astro::ADS::Search->new({
q => '...', # initial search query
fl => '...', # return list of attributes
});
my $result = $search->query();
my @papers = $result->papers();
while ( my $t = $result->next_query() ) {
$result = $search->query( $t );
push @papers, $result->get_papers();
}
while ( my $t = $result->next_query() ) {
push @papers, $result->more_papers( $t );
}
while ( push @papers, $result->next_query()->more_papers() ) {
}
=head1 DESCRIPTION
Search for papers in the Harvard ADS
You can put base terms in the creation of the object and use the
query method to add new terms to that query only
=head1 Methods
=head2 query
Adding a field key C<+q> to the query method B<adds> the query
term to the existing query terms,
whereas specifying a value for C<q> in the query method
overwrites the query terms and neglects gathering other search attributes,
such as authors or objects.
=head2 add_authors
Add a list of authors to a search query. Authors added here will not be
deleted if the query attribute is updated.
=head2 add_objects
Add a list of objects to a search query. Objects added here will not be
deleted if the query attribute is updated.
=head2 query_tree
Will return the L<Abstract Syntax Tree|https://ui.adsabs.harvard.edu/help/api/api-docs.html#get-/search/qtree> for the query.
=head2 bigquery
Accepts a L<list of many IDs|https://ui.adsabs.harvard.edu/help/api/api-docs.html#post-/search/bigquery> and supports paging.
=head2 Notes
From the ADS API, the "=" sign turns off the synonym expansion feature
available with the author and title fields
=head1 See Also
=over 4
=item * L<Astro::ADS>
=item * L<Astro::ADS::Result>
=item * L<ADS API|https://ui.adsabs.harvard.edu/help/api/>
=item * L<Search Syntax|https://ui.adsabs.harvard.edu/help/search/search-syntax>
=back
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2025 by Boyd Duffee.
This is free software, licensed under:
The MIT (X11) License
=cut