PrefixQuery, NestedQuery, BoolQuery, TermQuery, WildcardQuery, RangeQuery,
QueryStringQuery} from 'elastic-builder';
+const INDEX_SHORTCUTS_MAP = {
+ 'au:': 'author\\*:',
+ 'ti:': 'title\\*:',
+ 'su:': 'subject\\*:',
+ 'kw:': 'keyword\\*:',
+ 'se:': 'series\\*:',
+ 'pb:': 'identifier|publisher\\*:'
+};
+
@Injectable()
export class ElasticService {
enabled: boolean;
+ ebfMap: {[id: number]: IdlObject} = {};
constructor(
private idl: IdlService,
private pcrud: PcrudService
) {}
+ init(): Promise<any> {
+ return Promise.resolve();
+ }
+
// Returns true if Elastic can provide search results.
canSearch(ctx: CatalogSearchContext): boolean {
if (!this.enabled) { return false; }
return true;
}
- if (ctx.identSearch.isSearchable()
- && ctx.identSearch.queryType !== 'item_barcode') {
+ if (ctx.identSearch.isSearchable() &&
+ ctx.identSearch.queryType !== 'item_barcode' &&
+ ctx.identSearch.queryType !== 'identifier|tcn') {
return true;
}
let method = ctx.termSearch.isMetarecordSearch() ?
'open-ils.search.elastic.bib_search.metabib' :
- 'open-ils.search.elastic.bib_search'
+ 'open-ils.search.elastic.bib_search';
if (ctx.isStaff) { method += '.staff'; }
// Sort by match score by default.
search.sort(new Sort('_score', 'desc'));
}
+
+ // Apply a tie-breaker sort on bib ID.
+ search.sort(new Sort('id', 'desc'));
}
addFilters(ctx: CatalogSearchContext, rootNode: BoolQuery) {
ts.facetFilters.forEach(f => {
if (f.facetValue !== '') {
rootNode.filter(new TermQuery(
- `${f.facetClass}|${f.facetName}.facet`, f.facetValue));
+ `${f.facetClass}|${f.facetName}|facet`, f.facetValue));
}
});
const marcQuery = new BoolQuery();
const tag = ms.tags[idx];
const subfield = ms.subfields[idx];
+ const matchOp = ms.matchOp[idx];
- // Full-text search on the values
- const valMatch = new MultiMatchQuery(['marc.value*'], value);
- valMatch.operator('and');
- valMatch.type('most_fields');
- marcQuery.must(valMatch);
+ this.appendMatchOp(
+ marcQuery, matchOp, 'marc.value.text*', 'marc.value', value);
if (tag) {
marcQuery.must(new TermQuery('marc.tag', tag));
} else if (fieldClass === 'keyword' &&
matchOp === 'contains' && value.match(/:/)) {
+ // Map ti: to title\*: so the shortcut searches search
+ // across all sub-indexes.
+ let valueMod = value;
+ Object.keys(INDEX_SHORTCUTS_MAP).forEach(sc => {
+ const reg = new RegExp(sc, 'gi');
+ valueMod = valueMod.replace(reg, INDEX_SHORTCUTS_MAP[sc]);
+ });
+
// A search where 'keyword' 'contains' a value with a ':'
// character is assumed to be a complex / query string search.
// NOTE: could handle this differently, e.g. provide an escape
// character (e.g. !title:potter), a dedicated matchOp, etc.
boolNode.must(
- new QueryStringQuery(value)
+ new QueryStringQuery(valueMod)
.defaultOperator('AND')
.defaultField('keyword.text')
);
return;
}
- const textIndex = `${fieldClass}.text*`;
+ // KCLS ident searches: Identifier indices don't have text variations
+ let textIndex = fieldClass.match('identifier') ?
+ fieldClass : `${fieldClass}.text*`;
+
+ // Bib call numbers use different indexes depending on the matchOp
+ if (fieldClass === 'identifier|bibcn' &&
+ matchOp.match(/contains|nocontains|phrase/)) {
+ textIndex = 'keyword|bibcn.text*';
+ }
+
+ this.appendMatchOp(boolNode, matchOp, textIndex, fieldClass, value);
+ }
+
+ appendMatchOp(boolNode: BoolQuery, matchOp: string,
+ textIndex: string, termIndex: string, value: string) {
+
let query;
switch (matchOp) {
boolNode.mustNot(query);
return;
- // "exact" and "starts" searches use term searches instead
- // of full-text searches.
+ // "containsexact", "exact", "starts" searches use term
+ // searches instead of full-text searches.
+ case 'containsexact':
+ query = new WildcardQuery(termIndex, `*${value}*`);
+ boolNode.must(query);
+ return;
+
case 'exact':
- query = new TermQuery(fieldClass, value);
+ query = new TermQuery(termIndex, value);
boolNode.must(query);
return;
case 'starts':
- query = new PrefixQuery(fieldClass, value);
+ query = new PrefixQuery(termIndex, value);
boolNode.must(query);
return;
}