Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multilingual search #651

Merged
merged 5 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion conf/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ geonetwork4_api_url = "/geonetwork/srv/api"
proxy_path = ""
# This optional parameter defines, in which language metadata should be queried in elasticsearch.
# Use ISO 639-2/B (https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format to indicate the language of the metadata.
# Setting to "current" will use the current language of the User Interface.
# If not indicated, a wildcard is used and no language preference is applied for the search.
# metadata_language = "fre"
# metadata_language = "current"
# This optional URL should point to the login page that allows authentication to the datahub.
# If not indicated, the default geonetwork login page is used.
# The following three placeholders can be part of this URL:
Expand Down
17 changes: 9 additions & 8 deletions libs/api/repository/src/lib/gn4/elasticsearch/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ export const ES_SOURCE_SUMMARY = [
'userSavedCount',
]

export const ES_QUERY_STRING_FIELDS = [
'resourceTitleObject.${searchLang}^5',
'tag.${searchLang}^4',
'resourceAbstractObject.${searchLang}^3',
'lineageObject.${searchLang}^2',
'any.${searchLang}',
'uuid',
]
export type EsQueryFieldsPriorityType = Record<string, number>
export const ES_QUERY_FIELDS_PRIORITY = {
'resourceTitleObject.${searchLang}': 5,
'tag.${searchLang}': 4,
'resourceAbstractObject.${searchLang}': 3,
'lineageObject.${searchLang}': 2,
'any.${searchLang}': 1,
uuid: 1,
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { ElasticsearchService } from './elasticsearch.service'
import { ES_FIXTURE_AGGS_RESPONSE } from '@geonetwork-ui/common/fixtures'
import { EsSearchParams } from '../types/elasticsearch.model'
import { LangService } from '@geonetwork-ui/util/i18n'
import { EsSearchParams } from '@geonetwork-ui/api/metadata-converter'
import { TestBed } from '@angular/core/testing'
import { METADATA_LANGUAGE } from '../../metadata-language'

class LangServiceMock {
iso3 = 'eng'
}

describe('ElasticsearchService', () => {
let service: ElasticsearchService
let searchFilters

beforeEach(() => {
service = new ElasticsearchService('fre')
TestBed.configureTestingModule({
providers: [
{
provide: LangService,
useClass: LangServiceMock,
},
{
provide: METADATA_LANGUAGE,
useValue: 'fre',
},
],
})
service = TestBed.inject(ElasticsearchService)
})

it('should be created', () => {
Expand Down Expand Up @@ -356,6 +375,72 @@ describe('ElasticsearchService', () => {
})
})

describe('#injectLangInQueryStringFields - Search language', () => {
let queryStringFields = { 'resourceTitleObject.${searchLang}': 1 }
describe('When no lang from config', () => {
beforeEach(() => {
service['metadataLang'] = undefined
})
it('use * wildcard', () => {
expect(
service['injectLangInQueryStringFields'](queryStringFields)[0].split(
'.'
)[1]
).toEqual('*')
})
})
describe('When one lang in config', () => {
beforeEach(() => {
service['metadataLang'] = 'fre'
})
it('search in the config language', () => {
expect(
service['injectLangInQueryStringFields'](queryStringFields)[0].split(
'.'
)[1]
).toEqual('langfre')
})
})
describe('When "current" language from config"', () => {
beforeEach(() => {
service['metadataLang'] = 'current'
service['lang3'] = 'eng'
})
it('search in the UI language', () => {
expect(
service['injectLangInQueryStringFields'](queryStringFields)[0].split(
'.'
)[1]
).toEqual('langeng^11')
})
it('add * fallback with low priority', () => {
queryStringFields = {
'resourceTitleObject.${searchLang}': 5,
'tag.${searchLang}': 4,
'resourceAbstractObject.${searchLang}': 3,
'lineageObject.${searchLang}': 2,
'any.${searchLang}': 1,
uuid: 1,
}
expect(
service['injectLangInQueryStringFields'](queryStringFields)
).toEqual([
'resourceTitleObject.langeng^15',
'resourceTitleObject.*^5',
'tag.langeng^14',
'tag.*^4',
'resourceAbstractObject.langeng^13',
'resourceAbstractObject.*^3',
'lineageObject.langeng^12',
'lineageObject.*^2',
'any.langeng^11',
'any.*',
'uuid',
])
})
})
})

describe('#buildAutocompletePayload', () => {
describe('given an autocomplete config', () => {
it('returns the search payload', () => {
Expand All @@ -373,9 +458,9 @@ describe('ElasticsearchService', () => {
{
multi_match: {
fields: [
'resourceTitleObject.langfre',
'resourceAbstractObject.langfre',
'tag',
'resourceTitleObject.langfre^4',
'resourceAbstractObject.langfre^3',
'tag^2',
'resourceIdentifier',
],
query: 'blarg',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Inject, Injectable, Optional } from '@angular/core'
import { Geometry } from 'geojson'
import { ES_QUERY_STRING_FIELDS, ES_SOURCE_SUMMARY } from './constant'
import {
ES_QUERY_FIELDS_PRIORITY,
ES_SOURCE_SUMMARY,
EsQueryFieldsPriorityType,
} from './constant'
import {
Aggregation,
AggregationParams,
Expand All @@ -19,6 +23,7 @@ import {
SortParams,
TermsAggregationResult,
} from '@geonetwork-ui/api/metadata-converter'
import { LangService } from '@geonetwork-ui/util/i18n'

@Injectable({
providedIn: 'root',
Expand All @@ -27,8 +32,10 @@ export class ElasticsearchService {
// runtime fields are computed using a Painless script
// see: https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
private runtimeFields: Record<string, string> = {}
private lang3 = this.langService.iso3

constructor(
private langService: LangService,
@Optional() @Inject(METADATA_LANGUAGE) private metadataLang: string
) {}

Expand Down Expand Up @@ -168,13 +175,40 @@ export class ElasticsearchService {
}

private injectLangInQueryStringFields(
queryStringFields: string[],
lang: string
queryFieldsPriority: EsQueryFieldsPriorityType
) {
const queryLang = lang ? `lang${lang}` : `*`
return queryStringFields.map((field) => {
return field.replace(/\$\{searchLang\}/g, queryLang)
})
const queryLang = this.getQueryLang()
return Object.keys(queryFieldsPriority).reduce((query, field) => {
const multiLangRegExp = /\$\{searchLang\}/g
const isMultilangField = multiLangRegExp.test(field)
const fieldPriority = queryFieldsPriority[field]
return [
...query,
...(this.isCurrentSearchLang() && isMultilangField
? [
`${field.replace(multiLangRegExp, queryLang)}^${
fieldPriority + 10
}`,
field.replace(multiLangRegExp, '*') +
(fieldPriority > 1 ? `^${fieldPriority}` : ''),
]
: [
field.replace(multiLangRegExp, queryLang) +
(fieldPriority > 1 ? `^${fieldPriority}` : ''),
]),
]
}, [])
}

private getQueryLang(): string {
if (this.metadataLang) {
return this.isCurrentSearchLang()
? `lang${this.lang3}`
: `lang${this.metadataLang}`
} else return '*'
}
private isCurrentSearchLang() {
return this.metadataLang === 'current'
}

private buildPayloadQuery(
Expand Down Expand Up @@ -203,10 +237,7 @@ export class ElasticsearchService {
query_string: {
query: this.escapeSpecialCharacters(any),
default_operator: 'AND',
fields: this.injectLangInQueryStringFields(
ES_QUERY_STRING_FIELDS,
this.metadataLang
),
fields: this.injectLangInQueryStringFields(ES_QUERY_FIELDS_PRIORITY),
},
})
}
Expand Down Expand Up @@ -290,15 +321,12 @@ export class ElasticsearchService {
multi_match: {
query,
type: 'bool_prefix',
fields: this.injectLangInQueryStringFields(
[
'resourceTitleObject.${searchLang}',
'resourceAbstractObject.${searchLang}',
'tag',
'resourceIdentifier',
],
this.metadataLang
),
fields: this.injectLangInQueryStringFields({
'resourceTitleObject.${searchLang}': 4,
'resourceAbstractObject.${searchLang}': 3,
tag: 2,
resourceIdentifier: 1,
}),
},
},
],
Expand Down
Loading