Bug 9239: Allow the use of QueryParser for all queries

With the inclusion of this patch, all searches will (try) to use
QueryParser for handling queries for both the bibliographic and authority
databases if UseQueryParser is enabled. If QueryParser is unavailable,
UseQueryParser is disabled, or the search uses CCL indexes, the old
search code will be used.

To test:
1) Apply patch.
2) Run the unit test with `prove t/QueryParser.t`
3) Enable the UseQueryParser syspref.
4) Try searches that should return results in the following places:
   * OPAC (simple search)
   * OPAC (advanced search)
   * OPAC (authorities)
   * Staff client (header search)
   * Staff client (advanced search)
   * Staff client (cataloging search)
   * Staff client (authorities)
   * Staff client (importing a batch using a match point)
   * Staff client (searching for an item for adding to a label)
   * Staff client (acquisitions)
   * Staff client (searching for a record to create a serial)
   * ANYWHERE ELSE I HAVE FORGOTTEN
5) Disable the UseQueryParser syspref. Repeat at least some of the
   searches you did above.
6) If all searches worked, sign off.

Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz>
Signed-off-by: Elliott Davis <elliott@bywatersolions.com>
Searching still works as expected for variuos places.
QueryParser syspref seemed to be enabled by default

Signed-off-by: Katrin Fischer <Katrin.Fischer.83@web.de>
Signed-off-by: Jared Camins-Esakov <jcamins@cpbibliography.com>
This commit is contained in:
Jared Camins-Esakov 2012-12-11 12:52:40 -05:00
parent b086fcab9a
commit 144c7f4e4e
11 changed files with 237 additions and 91 deletions

View File

@ -201,6 +201,9 @@ sub SearchAuthorities {
}
} else {
my $query;
my $qpquery = '';
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $attr = '';
# the marclist may contain "mainentry". In this case, search the tag_to_report, that depends on
# the authtypecode. Then, search on $a of this tag_to_report
@ -219,6 +222,9 @@ sub SearchAuthorities {
if ($n>1){
while ($n>1){$query= "\@or ".$query;$n--;}
}
if ($QParser) {
$qpquery .= '(authtype:' . join('|| authtype:', @auths) . ')';
}
}
my $dosearch;
@ -276,6 +282,9 @@ sub SearchAuthorities {
$q2 .=$attr;
$dosearch=1;
++$attr_cnt;
if ($QParser) {
$qpquery .= " $tags->[$i]:$value->[$i]";
}
}#if value
}
##Add how many queries generated
@ -296,8 +305,21 @@ sub SearchAuthorities {
} elsif ($sortby eq 'AuthidDsc') {
$orderstring = '@attr 7=2 @attr 4=109 @attr 1=Local-Number 0';
}
$query=($query?$query:"\@attr 1=_ALLRECORDS \@attr 2=103 ''");
$query="\@or $orderstring $query" if $orderstring;
if ($QParser) {
$qpquery .= ' all:all' unless $value->[0];
if ( $value->[0] =~ m/^qp=(.*)$/ ) {
$qpquery = $1;
}
$qpquery .= " #$sortby";
$QParser->parse( $qpquery );
$query = $QParser->target_syntax('authorityserver');
} else {
$query=($query?$query:"\@attr 1=_ALLRECORDS \@attr 2=103 ''");
$query="\@or $orderstring $query" if $orderstring;
}
$offset=0 unless $offset;
my $counter = $offset;
@ -406,7 +428,7 @@ sub CountUsage {
} else {
### ZOOM search here
my $query;
$query= "an=".$authid;
$query= "an:".$authid;
my ($err,$res,$result) = C4::Search::SimpleSearch($query,0,10);
if ($err) {
warn "Error: $err from search $query";
@ -911,11 +933,19 @@ sub FindDuplicateAuthority {
$sth->finish;
# warn "record :".$record->as_formatted." auth_tag_to_report :$auth_tag_to_report";
# build a request for SearchAuthorities
my $query='at='.$authtypecode.' ';
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
my $query='at:'.$authtypecode.' ';
my $filtervalues=qr([\001-\040\!\'\"\`\#\$\%\&\*\+,\-\./:;<=>\?\@\(\)\{\[\]\}_\|\~]);
if ($record->field($auth_tag_to_report)) {
foreach ($record->field($auth_tag_to_report)->subfields()) {
$_->[1]=~s/$filtervalues/ /g; $query.= " and he,wrdl=\"".$_->[1]."\"" if ($_->[0]=~/[A-z]/);
$_->[1]=~s/$filtervalues/ /g; $query.= " $op he:\"".$_->[1]."\"" if ($_->[0]=~/[A-z]/);
}
}
my ($error, $results, $total_hits) = C4::Search::SimpleSearch( $query, 0, 1, [ "authorityserver" ] );

View File

@ -626,6 +626,8 @@ sub get_matches {
my %matches = ();
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
foreach my $matchpoint (@{ $self->{'matchpoints'} }) {
my @source_keys = _get_match_keys($source_record, $matchpoint);
next if scalar(@source_keys) == 0;
@ -634,31 +636,36 @@ sub get_matches {
my $error;
my $searchresults;
my $total_hits;
if ($self->{'record_type'} eq 'biblio') {
$query = join(" or ", map { "$matchpoint->{'index'}=$_" } @source_keys);
# FIXME only searching biblio index at the moment
if ($QParser) {
$query = join(" || ", map { "$matchpoint->{'index'}:$_" } @source_keys);
require C4::Search;
($error, $searchresults, $total_hits) = C4::Search::SimpleSearch($query, 0, $max_matches);
} elsif ($self->{'record_type'} eq 'authority') {
my $authresults;
my @marclist;
my @and_or;
my @excluding = [];
my @operator;
my @value;
foreach my $key (@source_keys) {
push @marclist, $matchpoint->{'index'};
push @and_or, 'or';
push @operator, 'exact';
push @value, $key;
}
require C4::AuthoritiesMarc;
($authresults, $total_hits) = C4::AuthoritiesMarc::SearchAuthorities(
\@marclist, \@and_or, \@excluding, \@operator,
\@value, 0, 20, undef, 'AuthidAsc', 1
);
foreach my $result (@$authresults) {
push @$searchresults, $result->{'authid'};
($error, $searchresults, $total_hits) = C4::Search::SimpleSearch($query, 0, $max_matches, [ $self->{'record_type'} . 'server' ] );
} else {
if ($self->{'record_type'} eq 'biblio') {
$query = join(" or ", map { "$matchpoint->{'index'}=$_" } @source_keys);
require C4::Search;
($error, $searchresults, $total_hits) = C4::Search::SimpleSearch($query, 0, $max_matches);
} elsif ($self->{'record_type'} eq 'authority') {
my $authresults;
my @marclist;
my @and_or;
my @excluding = [];
my @operator;
my @value;
foreach my $key (@source_keys) {
push @marclist, $matchpoint->{'index'};
push @and_or, 'or';
push @operator, 'exact';
push @value, $key;
}
require C4::AuthoritiesMarc;
($authresults, $total_hits) = C4::AuthoritiesMarc::SearchAuthorities(
\@marclist, \@and_or, \@excluding, \@operator,
\@value, 0, 20, undef, 'AuthidAsc', 1
);
foreach my $result (@$authresults) {
push @$searchresults, $result->{'authid'};
}
}
}

View File

@ -100,9 +100,25 @@ sub FindDuplicate {
if ( $result->{isbn} ) {
$result->{isbn} =~ s/\(.*$//;
$result->{isbn} =~ s/\s+$//;
$query = "isbn=$result->{isbn}";
$query = "isbn:$result->{isbn}";
}
else {
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $titleindex;
my $authorindex;
my $op;
if ($QParser) {
$titleindex = 'title|exact';
$authorindex = 'author|exact';
$op = '&&';
} else {
$titleindex = 'ti,ext';
$authorindex = 'au,ext';
$op = 'and';
}
$result->{title} =~ s /\\//g;
$result->{title} =~ s /\"//g;
$result->{title} =~ s /\(//g;
@ -111,7 +127,7 @@ sub FindDuplicate {
# FIXME: instead of removing operators, could just do
# quotes around the value
$result->{title} =~ s/(and|or|not)//g;
$query = "ti,ext=$result->{title}";
$query = "$titleindex:\"$result->{title}\"";
if ( $result->{author} ) {
$result->{author} =~ s /\\//g;
$result->{author} =~ s /\"//g;
@ -120,7 +136,7 @@ sub FindDuplicate {
# remove valid operators
$result->{author} =~ s/(and|or|not)//g;
$query .= " and au,ext=$result->{author}";
$query .= " $op $authorindex:\"$result->{author}\"";
}
}
@ -224,11 +240,21 @@ sub SimpleSearch {
my $results = [];
my $total_hits = 0;
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') && ! ($query =~ m/\w,\w|\w=\w/));
# Initialize & Search Zebra
for ( my $i = 0 ; $i < @servers ; $i++ ) {
eval {
$zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
$zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
if ($QParser) {
$query =~ s/=/:/g;
$QParser->parse( $query );
$query = $QParser->target_syntax($servers[$i]);
$zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
} else {
$zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
}
$tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
# error handling
@ -1091,7 +1117,9 @@ on authority data).
=cut
sub _handle_exploding_index {
my ( $index, $term ) = @_;
my ($QParser, $struct, $filter, $params, $negate, $server) = @_;
my $index = $filter;
my $term = join(' ', @$params);
return unless ($index =~ m/(su-br|su-na|su-rl)/ && $term);
@ -1099,8 +1127,8 @@ sub _handle_exploding_index {
my $codesubfield = $marcflavour eq 'UNIMARC' ? '5' : 'w';
my $wantedcodes = '';
my @subqueries = ( "(su=\"$term\")");
my ($error, $results, $total_hits) = SimpleSearch( "Heading,wrdl=$term", undef, undef, [ "authorityserver" ] );
my @subqueries = ( "su:\"$term\"");
my ($error, $results, $total_hits) = SimpleSearch( "he:$term", undef, undef, [ "authorityserver" ] );
foreach my $auth (@$results) {
my $record = MARC::Record->new_from_usmarc($auth);
my @references = $record->field('5..');
@ -1114,11 +1142,13 @@ sub _handle_exploding_index {
}
foreach my $reference (@references) {
my $codes = $reference->subfield($codesubfield);
push @subqueries, '(su="' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '")' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
push @subqueries, 'su:"' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '"' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
}
}
}
return join(' or ', @subqueries);
my $query = '(' x scalar(@subqueries) . join(') || ', @subqueries) . ')';
warn $query;
return $query;
}
=head2 parseQuery
@ -1145,37 +1175,42 @@ sub parseQuery {
my $query = $operands[0];
my $index;
my $term;
my $query_desc;
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') || $query =~ s/^qp=//);
undef $QParser if ($query =~ m/^(ccl=|pqf=|cql=)/ || grep (/\w,\w|\w=\w/, @operands) );
if ($QParser)
{
$query = '';
for ( my $ii = 0 ; $ii <= @operands ; $ii++ ) {
next unless $operands[$ii];
$query .= $operators[ $ii - 1 ] eq 'or' ? ' || ' : ' && '
if ($query);
$query .=
( $operators[$ii] ? "$operators[$ii]:" : '' ) . $operands[$ii];
}
foreach my $limit (@limits) {
}
foreach my $modifier (@sort_by) {
$query .= " #$modifier";
}
$query_desc = $query;
if ( C4::Context->preference("QueryWeightFields") ) {
}
$QParser->add_bib1_filter_map( 'biblioserver', 'su-br', { 'callback' => \&_handle_exploding_index });
$QParser->add_bib1_filter_map( 'biblioserver', 'su-na', { 'callback' => \&_handle_exploding_index });
$QParser->add_bib1_filter_map( 'biblioserver', 'su-rl', { 'callback' => \&_handle_exploding_index });
$QParser->parse( $query );
$operands[0] = "pqf=" . $QParser->target_syntax('biblioserver');
# TODO: once we are using QueryParser, all this special case code for
# exploded search indexes will be replaced by a callback to
# _handle_exploding_index
if ( $query =~ m/^(.*)\b(su-br|su-na|su-rl)[:=](\w.*)$/ ) {
$query = $1;
$index = $2;
$term = $3;
} else {
$query = '';
for ( my $i = 0 ; $i <= @operands ; $i++ ) {
if ($operands[$i] && $indexes[$i] =~ m/(su-br|su-na|su-rl)/) {
$index = $indexes[$i];
$term = $operands[$i];
} elsif ($operands[$i]) {
$query .= $operators[$i] eq 'or' ? ' or ' : ' and ' if ($query);
$query .= "($indexes[$i]:$operands[$i])";
}
}
}
if ($index) {
my $queryPart = _handle_exploding_index($index, $term);
if ($queryPart) {
$query .= "($queryPart)";
}
$operators = ();
$operands[0] = "ccl=$query";
}
return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang);
return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc);
}
=head2 buildQuery
@ -1199,7 +1234,8 @@ sub buildQuery {
warn "---------\nEnter buildQuery\n---------" if $DEBUG;
( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = parseQuery($operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
my $query_desc;
( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc) = parseQuery($operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
# dereference
my @operators = $operators ? @$operators : ();
@ -1227,7 +1263,6 @@ sub buildQuery {
# initialize the variables we're passing back
my $query_cgi;
my $query_desc;
my $query_type;
my $limit;
@ -1262,7 +1297,13 @@ sub buildQuery {
return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
}
if ( $query =~ /^pqf=/ ) {
return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
if ($query_desc) {
$query_cgi = "q=$query_desc";
} else {
$query_desc = $';
$query_cgi = "q=pqf=$'";
}
return ( undef, $', $', $query_cgi, $query_desc, '', '', '', '', 'pqf' );
}
# pass nested queries directly

View File

@ -85,7 +85,7 @@ sub get_suggestions {
# FIXME: calling into C4
require C4::AuthoritiesMarc;
my ( $searchresults, $count ) = C4::AuthoritiesMarc::SearchAuthorities(
\@marclist, \@and_or, \@excluding, \@operator,
@marclist, @and_or, @excluding, @operator,
@value, 0, $param->{'count'}, '',
'Relevance', 0
);

View File

@ -70,10 +70,16 @@ if ($query) {
# build query
my @operands = $query;
my ( $builterror,$builtquery,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$stopwords_removed,$query_type) = buildQuery(undef,\@operands);
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
unless ($QParser) {
my ( $builterror,$builtquery,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$stopwords_removed,$query_type) = buildQuery(undef,\@operands);
$query = $builtquery;
}
# find results
my ( $error, $marcresults, $total_hits ) = SimpleSearch($builtquery, $results_per_page * ($page - 1), $results_per_page);
my ( $error, $marcresults, $total_hits ) = SimpleSearch($query, $results_per_page * ($page - 1), $results_per_page);
if ( defined $error ) {
$template->param( error => $error );

View File

@ -190,7 +190,15 @@ sub plugin {
my $startfrom = $query->param('startfrom');
my $resultsperpage = $query->param('resultsperpage') || 20;
my $orderby;
$search = 'kw,wrdl=' . $search . ' and mc-itemtype=' . $itype if $itype;
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
$search = 'kw:' . $search . " $op mc-itemtype:" . $itype if $itype;
my ( $errors, $results, $total_hits ) =
SimpleSearch( $search, $startfrom * $resultsperpage,
$resultsperpage );

View File

@ -341,7 +341,15 @@ sub plugin {
my $startfrom = $query->param('startfrom');
my $resultsperpage = $query->param('resultsperpage') || 20;
my $orderby;
$search = 'kw,wrdl='.$search.' and mc-itemtype='.$itype if $itype;
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
$search = 'kw:'.$search." $op mc-itemtype:".$itype if $itype;
my ( $errors, $results, $total_hits ) = SimpleSearch($search, $startfrom * $resultsperpage, $resultsperpage );
if (defined $errors ) {
$results = [];

View File

@ -66,10 +66,12 @@ my $display_columns = [ {_add => {label => "Add Item", link_fi
];
if ( $op eq "do_search" ) {
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
$idx = $query->param('idx');
$ccl_textbox = $query->param('ccl_textbox');
if ( $ccl_textbox && $idx ) {
$ccl_query = "$idx=$ccl_textbox";
$ccl_query = "$idx:$ccl_textbox";
}
$datefrom = $query->param('datefrom');
@ -77,15 +79,26 @@ if ( $op eq "do_search" ) {
if ($datefrom) {
$datefrom = C4::Dates->new($datefrom);
$ccl_query .= ' and ' if $ccl_textbox;
$ccl_query .=
"acqdate,st-date-normalized,ge=" . $datefrom->output("iso");
if ($QParser) {
$ccl_query .= ' && ' if $ccl_textbox;
$ccl_query .=
"acqdate(" . $datefrom->output("iso") . '-)';
} else {
$ccl_query .= ' and ' if $ccl_textbox;
$ccl_query .=
"acqdate,st-date-normalized,ge=" . $datefrom->output("iso");
}
}
if ($dateto) {
$dateto = C4::Dates->new($dateto);
$ccl_query .= ' and ' if ( $ccl_textbox || $datefrom );
$ccl_query .= "acqdate,st-date-normalized,le=" . $dateto->output("iso");
if ($QParser) {
$ccl_query .= ' && ' if ( $ccl_textbox || $datefrom );
$ccl_query .= "acqdate(-" . $dateto->output("iso") . ')';
} else {
$ccl_query .= ' and ' if ( $ccl_textbox || $datefrom );
$ccl_query .= "acqdate,st-date-normalized,le=" . $datefrom->output("iso");
}
}
my $offset = $startfrom > 1 ? $startfrom - 1 : 0;

View File

@ -432,7 +432,15 @@ sub build_query {
my $string = build_simplequery($matchingpoint,$record);
push @searchstrings,$string if (length($string)>0);
}
return join(" and ",@searchstrings);
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
return join(" $op ",@searchstrings);
}
sub build_simplequery {
my $element=shift;
@ -442,10 +450,18 @@ sub build_simplequery {
my @searchstrings;
foreach my $field ($record->field($tag)){
if (length($field->as_string("$subfields"))>0){
push @searchstrings,"$index,wrdl=\"".$field->as_string("$subfields")."\"";
push @searchstrings,"$index:\"".$field->as_string("$subfields")."\"";
}
}
return join(" and ",@searchstrings);
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
return join(" $op ",@searchstrings);
}
sub report_item_errors {
my $biblionumber = shift;

View File

@ -90,11 +90,19 @@ if ($op eq "do_search" && $query) {
# add the itemtype limit if applicable
my $itemtypelimit = $input->param('itemtypelimit');
if ( $itemtypelimit ) {
if (!$advanced_search_types or $advanced_search_types eq 'itemtypes') {
$query .= " AND $itype_or_itemtype=$itemtypelimit";
} else {
$query .= " AND $advanced_search_types=$itemtypelimit";
}
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $op;
if ($QParser) {
$op = '&&';
} else {
$op = 'and';
}
if (!$advanced_search_types or $advanced_search_types eq 'itemtypes') {
$query .= " $op $itype_or_itemtype:$itemtypelimit";
} else {
$query .= " $op $advanced_search_types:$itemtypelimit";
}
}
$debug && warn $query;
$resultsperpage= $input->param('resultsperpage');

View File

@ -32,6 +32,7 @@ my $QueryAutoTruncate = 0;
my $QueryWeightFields = 0;
my $QueryFuzzy = 0;
my $QueryRemoveStopwords = 0;
my $UseQueryParser = 0;
my $contextmodule = new Test::MockModule('C4::Context');
$contextmodule->mock('_new_dbh', sub {
my $dbh = DBI->connect( 'DBI:Mock:', '', '' )
@ -53,6 +54,8 @@ $contextmodule->mock('preference', sub {
return $QueryFuzzy;
} elsif ($pref eq 'QueryRemoveStopwords') {
return $QueryRemoveStopwords;
} elsif ($pref eq 'UseQueryParser') {
return $UseQueryParser;
} elsif ($pref eq 'maxRecordsForFacets') {
return 20;
} elsif ($pref eq 'FacetLabelTruncationLength') {
@ -109,6 +112,11 @@ $contextmodule->mock('marcfromkohafield', sub {
);
return \%hash;
});
$contextmodule->mock('queryparser', sub {
my $QParser = Koha::QueryParser::Driver::PQF->new();
$QParser->load_config("$datadir/etc/searchengine/queryparser.yaml");
return $QParser;
});
my $context = new C4::Context("$datadir/etc/koha-conf.xml");
$context->set_context();
@ -521,7 +529,7 @@ my $searchmodule = new Test::MockModule('C4::Search');
$searchmodule->mock('SimpleSearch', sub {
my $query = shift;
is($query, "Heading,wrdl=$term", "Searching for expected term '$term' for exploding") or return '', [], 0;
is($query, "he:$term", "Searching for expected term '$term' for exploding") or return '', [], 0;
my $record = MARC::Record->new;
if ($query =~ m/Arizona/) {
@ -538,6 +546,7 @@ $searchmodule->mock('SimpleSearch', sub {
return '', [ $record->as_usmarc() ], 1;
});
$UseQueryParser = 1;
$term = 'Arizona';
( $error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
@ -568,28 +577,28 @@ like($query, qr/history/, "Order of terms doesn't matter for advanced search");
( $error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$stopwords_removed, $query_type ) = buildQuery([], [ "su-br:$term" ], [ ], [ ], [], 0, 'en');
$stopwords_removed, $query_type ) = buildQuery([], [ "su-br($term)" ], [ ], [ ], [], 0, 'en');
matchesExplodedTerms("Simple search for broader subjects", $query, 'Arizona', 'United States');
( $error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$stopwords_removed, $query_type ) = buildQuery([], [ "su-na:$term" ], [ ], [ ], [], 0, 'en');
$stopwords_removed, $query_type ) = buildQuery([], [ "su-na($term)" ], [ ], [ ], [], 0, 'en');
matchesExplodedTerms("Simple search for narrower subjects", $query, 'Arizona', 'Maricopa County', 'Navajo County', 'Pima County');
( $error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$stopwords_removed, $query_type ) = buildQuery([], [ "su-rl:$term" ], [ ], [ ], [], 0, 'en');
$stopwords_removed, $query_type ) = buildQuery([], [ "su-rl($term)" ], [ ], [ ], [], 0, 'en');
matchesExplodedTerms("Simple search for related subjects", $query, 'Arizona', 'United States', 'Maricopa County', 'Navajo County', 'Pima County');
( $error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$stopwords_removed, $query_type ) = buildQuery([], [ "history and su-rl:$term" ], [ ], [ ], [], 0, 'en');
$stopwords_removed, $query_type ) = buildQuery([], [ "history && su-rl($term)" ], [ ], [ ], [], 0, 'en');
matchesExplodedTerms("Simple search for related subjects and keyword 'history' searches related subjects", $query, 'Arizona', 'United States', 'Maricopa County', 'Navajo County', 'Pima County');
like($query, qr/history/, "Simple search for related subjects and keyword 'history' searches for 'history'");
sub matchesExplodedTerms {
my ($message, $query, @terms) = @_;
my $match = "(( or )?\\((" . join ('|', map { "su=\"$_\"" } @terms) . ")\\)){" . scalar(@terms) . "}";
my $match = '(' . join ('|', map { " \@attr 1=Subject \@attr 4=1 \"$_\"" } @terms) . "){" . scalar(@terms) . "}";
like($query, qr/$match/, $message);
}