let method = 'open-ils.search.biblio.marc';
if (ctx.isStaff) { method += '.staff'; }
+ const method = ctx.getApiName();
+
const queryStruct = ctx.compileMarcSearchArgs();
return this.net.request('open-ils.search', method, queryStruct)
console.debug('search query', JSON.stringify(fullQuery));
- const method = ctx.getApiName();
+ let method = ctx.getApiName();
+ if (method === null) {
+ method = 'open-ils.search.biblio.multiclass.query';
+
+ if (ctx.termSearch.groupByMetarecord
+ && !ctx.termSearch.fromMetarecord) {
+ method = 'open-ils.search.metabib.multiclass.query';
+ }
+
+ if (ctx.isStaff) {
+ method += '.staff';
+ }
+ }
return this.net.request(
'open-ils.search', method, {
available: boolean;
sort: any[] = [];
searches: any[] = [];
+ marc_searches: any[] = [];
filters: any[] = [];
}
export class ElasticSearchContext extends CatalogSearchContext {
-
// The UI is ambiguous re: mixing ANDs and ORs.
// Here booleans are grouped ANDs first, then each OR is given its own node.
compileTerms(params: ElasticSearchParams) {
});
}
- addFilter(params: ElasticSearchParams, name: string, value: any) {
+ addTermFilter(params: ElasticSearchParams, name: string, value: any) {
if (value === '' ||
value === null ||
value === undefined) { return; }
compileTermSearchQuery(): any {
const ts = this.termSearch;
- const params = new ElasticSearchParams();
+ const params = this.newParams();
params.available = ts.available;
- if (this.sort) {
- // e.g. title, title.descending => [{title => 'desc'}]
- const parts = this.sort.split(/\./);
- const sort: any = {};
- sort[parts[0]] = parts[1] ? 'desc' : 'asc';
- params.sort = [sort];
- }
-
if (ts.date1 && ts.dateOp) {
const dateFilter: Object = {};
switch (ts.dateOp) {
case 'is':
- this.addFilter(params, 'date1', ts.date1);
+ this.addTermFilter(params, 'date1', ts.date1);
break;
case 'before':
params.filters.push({range: {date1: {lt: ts.date1}}});
}
this.compileTerms(params);
- params.search_org = this.searchOrg.id();
if (this.global) {
params.search_depth = this.org.root().ou_type().depth();
*/
if (ts.format) {
- this.addFilter(params, ts.formatCtype, ts.format);
+ this.addTermFilter(params, ts.formatCtype, ts.format);
}
Object.keys(ts.ccvmFilters).forEach(field => {
ts.ccvmFilters[field].forEach(value => {
if (value !== '') {
- this.addFilter(params, field, value);
+ this.addTermFilter(params, field, value);
}
});
});
ts.facetFilters.forEach(f => {
- this.addFilter(params,
+ this.addTermFilter(params,
`${f.facetClass}|${f.facetName}`, f.facetValue);
});
return params;
}
+ newParams(): ElasticSearchParams {
+ const params = new ElasticSearchParams();
+ params.limit = this.pager.limit;
+ params.offset = this.pager.offset;
+ params.search_org = this.searchOrg.id()
+
+ if (this.sort) {
+ // e.g. title, title.descending => [{title => 'desc'}]
+ const parts = this.sort.split(/\./);
+ const sort: any = {};
+ sort[parts[0]] = parts[1] ? 'desc' : 'asc';
+ params.sort = [sort];
+ }
+
+ return params;
+ }
+
+ compileMarcSearchArgs(): any {
+ const ms = this.marcSearch;
+ const params = this.newParams();
+
+ ms.values.forEach((val, idx) => {
+ if (val !== '') {
+ params.marc_searches.push({
+ tag: ms.tags[idx],
+ subfield: ms.subfields[idx] ? ms.subfields[idx] : null,
+ value: ms.values[idx]
+ });
+ }
+ });
+
+ return params;
+ }
+
getApiName(): string {
// Elastic covers only a subset of available search types.
- if (!this.termSearch.isSearchable() ||
- this.termSearch.groupByMetarecord ||
- this.termSearch.fromMetarecord
+ if (this.marcSearch.isSearchable() ||
+ (
+ this.termSearch.isSearchable() &&
+ !this.termSearch.groupByMetarecord &&
+ !this.termSearch.fromMetarecord
+ )
) {
- return super.getApiName();
- }
- return this.isStaff ?
- 'open-ils.search.elastic.bib_search.staff' :
- 'open-ils.search.elastic.bib_search';
+ return this.isStaff ?
+ 'open-ils.search.elastic.bib_search.staff' :
+ 'open-ils.search.elastic.bib_search';
+ }
+
+ // Fall back to existing APIs.
+ return super.getApiName();
}
}
}
getApiName(): string {
- let method = 'open-ils.search.biblio.multiclass.query';
-
- if (this.termSearch.groupByMetarecord
- && !this.termSearch.fromMetarecord) {
- method = 'open-ils.search.metabib.multiclass.query';
- }
-
- if (this.isStaff) {
- method += '.staff';
- }
-
- return method;
+ return null;
}
}
use OpenSRF::Utils::SettingsClient;
use OpenILS::Utils::CStoreEditor q/:funcs/;
use OpenILS::Elastic::BibSearch;
-use OpenILS::Elastic::BibMarc;
use List::Util qw/min/;
use OpenILS::Application::AppUtils;
append_search_nodes($elastic, $_) for @{$query->{searches}};
+ append_marc_nodes($elastic, $_) for @{$query->{marc_searches}};
+
add_elastic_holdings_filter($elastic, $staff,
$query->{search_org}, $query->{search_depth}, $query->{available});
}
-sub compile_elastic_marc_query {
- my ($args, $staff, $offset, $limit) = @_;
-
- # args->{searches} =
- # [{term => "harry", restrict => [{tag => 245, subfield => "a"}]}]
-
- my $root_and = [];
- for my $search (@{$args->{searches}}) {
-
- # NOTE Assume only one tag/subfield will be queried per search term.
- my $tag = $search->{restrict}->[0]->{tag};
- my $sf = $search->{restrict}->[0]->{subfield};
- my $value = $search->{term};
-
- # Use text searching on the value field
- my $value_query = {
- bool => {
- should => [
- {match => {'marc.value.text' =>
- {query => $value, operator => 'and'}}},
- {match => {'marc.value.text_folded' =>
- {query => $value, operator => 'and'}}}
- ]
- }
- };
-
- my @must = ($value_query);
-
- # tag (ES-only) and subfield are both optional
- push (@must, {term => {'marc.tag' => $tag}}) if $tag;
- push (@must, {term => {'marc.subfield' => $sf}}) if $sf && $sf ne '_';
+sub append_marc_nodes {
+ my ($marc_search) = @_;
- my $sub_query = {bool => {must => \@must}};
+ my $tag = $marc_search->{tag};
+ my $sf = $marc_search->{subfield};
+ my $value = $marc_search->{value};
- push (@$root_and, {
- nested => {
- path => 'marc',
- query => {bool => {must => $sub_query}}
- }
- });
- }
-
- return {
- _source => ['id'], # Fetch bib ID only
- size => $limit,
- from => $offset,
- sort => [],
- query => {
- bool => {
- must => $root_and,
- filter => []
- }
+ # Use text searching on the value field
+ my $value_query = {
+ bool => {
+ should => [
+ {match => {'marc.value.text' =>
+ {query => $value, operator => 'and'}}},
+ {match => {'marc.value.text_folded' =>
+ {query => $value, operator => 'and'}}}
+ ]
}
};
-}
+ my @must = ($value_query);
+ # tag (ES-only) and subfield are both optional
+ push (@must, {term => {'marc.tag' => $tag}}) if $tag;
+ push (@must, {term => {'marc.subfield' => $sf}}) if $sf;
-# Translate a MARC search API call into something consumable by Elasticsearch
-# Translate search results into a structure consistent with a bib search
-# API response.
-# TODO: This version is not currently holdings-aware, meaning it will return
-# results for all non-deleted bib records that match the query.
-sub marc_search {
- my ($class, $args, $staff, $limit, $offset) = @_;
-
- return {count => 0, ids => []}
- unless $args->{searches} && @{$args->{searches}};
-
- my $elastic_query =
- compile_elastic_marc_query($args, $staff, $offset, $limit);
-
- my $es = OpenILS::Elastic::BibMarc->new('main');
-
- $es->connect;
- my $results = $es->search($elastic_query);
-
- $logger->debug("ES elasticsearch returned: ".
- OpenSRF::Utils::JSON->perl2JSON($results));
-
- return {count => 0, ids => []} unless $results;
-
- my @bib_ids = map {$_->{_id}}
- grep {defined $_} @{$results->{hits}->{hits}};
+ my $sub_query = {bool => {must => \@must}};
return {
- ids => \@bib_ids,
- count => $results->{hits}->{total}
+ nested => {
+ path => 'marc',
+ query => {bool => {must => $sub_query}}
+ }
};
}
-
-
1;
use OpenSRF::Utils::SettingsClient;
use OpenILS::Utils::CStoreEditor q/:funcs/;
use OpenILS::Elastic::Bib::Search;
-use OpenILS::Elastic::Bib::Marc;
+#use OpenILS::Elastic::Bib::Marc;
use List::Util qw/min/;
use Digest::MD5 qw(md5_hex);
}
}
}
+
};
sub index_name {