oarepo-runtime 1.4.11__py3-none-any.whl → 1.4.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oarepo_runtime/cf/__init__.py +32 -2
- oarepo_runtime/cf/cli.py +2 -11
- oarepo_runtime/cf/mappings.py +22 -35
- oarepo_runtime/cli/index.py +0 -1
- oarepo_runtime/records/__init__.py +29 -0
- oarepo_runtime/records/icu.py +132 -0
- oarepo_runtime/relations/errors.py +3 -1
- oarepo_runtime/services/icu.py +160 -0
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/METADATA +40 -1
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/RECORD +14 -13
- oarepo_runtime/cf/icu.py +0 -92
- oarepo_runtime/services/search.py +0 -108
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/LICENSE +0 -0
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/WHEEL +0 -0
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/entry_points.txt +0 -0
- {oarepo_runtime-1.4.11.dist-info → oarepo_runtime-1.4.13.dist-info}/top_level.txt +0 -0
oarepo_runtime/cf/__init__.py
CHANGED
@@ -1,15 +1,45 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
1
3
|
from flask import current_app
|
2
4
|
from invenio_records.systemfields import DictField, SystemField
|
5
|
+
from invenio_records_resources.services.custom_fields import BaseCF
|
6
|
+
|
7
|
+
from oarepo_runtime.records import MappingSystemFieldMixin
|
3
8
|
|
4
9
|
|
5
|
-
class CustomFieldsMixin:
|
10
|
+
class CustomFieldsMixin(MappingSystemFieldMixin):
|
6
11
|
def __init__(self, config_key, *args, **kwargs) -> None:
|
7
12
|
super().__init__(*args, **kwargs)
|
8
13
|
self.config_key = config_key
|
9
14
|
|
15
|
+
@property
|
16
|
+
def mapping(self):
|
17
|
+
custom_fields: List[BaseCF] = current_app.config[self.config_key]
|
18
|
+
return {cf.name: cf.mapping for cf in custom_fields}
|
19
|
+
|
20
|
+
@property
|
21
|
+
def mapping_settings(self):
|
22
|
+
return {}
|
23
|
+
|
24
|
+
def search_dump(self, data):
|
25
|
+
custom_fields = current_app.config.get(self.config_key, {})
|
26
|
+
|
27
|
+
for cf in custom_fields:
|
28
|
+
cf.dump(data, cf_key=self.key)
|
29
|
+
return data
|
30
|
+
|
31
|
+
def search_load(self, data):
|
32
|
+
custom_fields = current_app.config.get(self.config_key, {})
|
33
|
+
|
34
|
+
for cf in custom_fields:
|
35
|
+
cf.load(data, cf_key=self.key)
|
36
|
+
return data
|
37
|
+
|
10
38
|
|
11
39
|
class CustomFields(CustomFieldsMixin, DictField):
|
12
|
-
|
40
|
+
@property
|
41
|
+
def mapping(self):
|
42
|
+
return {self.key: {"type": "object", "properties": super().mapping}}
|
13
43
|
|
14
44
|
|
15
45
|
class InlinedCustomFields(CustomFieldsMixin, SystemField):
|
oarepo_runtime/cf/cli.py
CHANGED
@@ -12,15 +12,6 @@ def cf():
|
|
12
12
|
|
13
13
|
|
14
14
|
@cf.command(name="init", help="Prepare custom fields in indices")
|
15
|
-
@click.option(
|
16
|
-
"-f",
|
17
|
-
"--field-name",
|
18
|
-
"field_names",
|
19
|
-
type=str,
|
20
|
-
required=False,
|
21
|
-
multiple=True,
|
22
|
-
help="A custom field name to create. If not provided, all custom fields will be created.",
|
23
|
-
)
|
24
15
|
@with_appcontext
|
25
|
-
def init(
|
26
|
-
prepare_cf_indices(
|
16
|
+
def init():
|
17
|
+
prepare_cf_indices()
|
oarepo_runtime/cf/mappings.py
CHANGED
@@ -2,23 +2,18 @@ import inspect
|
|
2
2
|
from typing import Iterable, List
|
3
3
|
|
4
4
|
import click
|
5
|
-
|
5
|
+
import deepmerge
|
6
6
|
from invenio_records_resources.proxies import current_service_registry
|
7
|
-
from invenio_records_resources.services.custom_fields import BaseCF
|
8
7
|
from invenio_records_resources.services.custom_fields.mappings import (
|
9
8
|
Mapping as InvenioMapping,
|
10
9
|
)
|
11
|
-
from invenio_records_resources.services.custom_fields.validate import (
|
12
|
-
validate_custom_fields,
|
13
|
-
)
|
14
10
|
from invenio_records_resources.services.records.config import RecordServiceConfig
|
15
11
|
from invenio_records_resources.services.records.service import RecordService
|
16
12
|
from invenio_search import current_search_client
|
17
13
|
from invenio_search.engine import dsl, search
|
18
14
|
from invenio_search.utils import build_alias_name
|
19
15
|
|
20
|
-
from oarepo_runtime.
|
21
|
-
import deepmerge
|
16
|
+
from oarepo_runtime.records import MappingSystemFieldMixin
|
22
17
|
|
23
18
|
|
24
19
|
class Mapping(InvenioMapping):
|
@@ -69,34 +64,22 @@ class Mapping(InvenioMapping):
|
|
69
64
|
|
70
65
|
# pieces taken from https://github.com/inveniosoftware/invenio-rdm-records/blob/master/invenio_rdm_records/cli.py
|
71
66
|
# as cf initialization is not supported directly in plain invenio
|
72
|
-
def prepare_cf_indices(
|
67
|
+
def prepare_cf_indices():
|
73
68
|
service: RecordService
|
74
69
|
for service in current_service_registry._services.values():
|
75
70
|
config: RecordServiceConfig = service.config
|
76
|
-
prepare_cf_index(config
|
71
|
+
prepare_cf_index(config)
|
77
72
|
|
78
73
|
|
79
|
-
def prepare_cf_index(config: RecordServiceConfig
|
74
|
+
def prepare_cf_index(config: RecordServiceConfig):
|
80
75
|
record_class = getattr(config, "record_cls", None)
|
81
76
|
if not record_class:
|
82
77
|
return
|
83
78
|
|
84
|
-
|
85
|
-
# validate them
|
86
|
-
for field_name, available_fields in get_custom_fields(record_class):
|
87
|
-
validate_custom_fields(
|
88
|
-
given_fields=field_names, available_fields=available_fields, namespaces=[]
|
89
|
-
)
|
90
|
-
|
79
|
+
for fld in get_mapping_fields(record_class):
|
91
80
|
# get mapping
|
92
|
-
|
93
|
-
|
94
|
-
)
|
95
|
-
settings = Mapping.settings_for_fields(
|
96
|
-
field_names, available_fields, field_name=field_name
|
97
|
-
)
|
98
|
-
if not properties:
|
99
|
-
continue
|
81
|
+
mapping = fld.mapping
|
82
|
+
settings = fld.mapping_settings
|
100
83
|
|
101
84
|
# upload mapping
|
102
85
|
try:
|
@@ -106,12 +89,7 @@ def prepare_cf_index(config: RecordServiceConfig, field_names: List[str] = None)
|
|
106
89
|
),
|
107
90
|
using=current_search_client,
|
108
91
|
)
|
109
|
-
|
110
|
-
record_index.close()
|
111
|
-
record_index.put_settings(body=settings)
|
112
|
-
record_index.open()
|
113
|
-
if properties:
|
114
|
-
record_index.put_mapping(body={"properties": properties})
|
92
|
+
update_index(record_index, settings, mapping)
|
115
93
|
|
116
94
|
if hasattr(config, "draft_cls"):
|
117
95
|
draft_index = dsl.Index(
|
@@ -120,15 +98,24 @@ def prepare_cf_index(config: RecordServiceConfig, field_names: List[str] = None)
|
|
120
98
|
),
|
121
99
|
using=current_search_client,
|
122
100
|
)
|
123
|
-
draft_index
|
101
|
+
update_index(draft_index, settings, mapping)
|
124
102
|
|
125
103
|
except search.RequestError as e:
|
126
104
|
click.secho("An error occurred while creating custom fields.", fg="red")
|
127
105
|
click.secho(e.info["error"]["reason"], fg="red")
|
128
106
|
|
129
107
|
|
130
|
-
def
|
108
|
+
def update_index(record_index, settings, mapping):
|
109
|
+
if settings:
|
110
|
+
record_index.close()
|
111
|
+
record_index.put_settings(body=settings)
|
112
|
+
record_index.open()
|
113
|
+
if mapping:
|
114
|
+
record_index.put_mapping(body={"properties": mapping})
|
115
|
+
|
116
|
+
|
117
|
+
def get_mapping_fields(record_class) -> Iterable[MappingSystemFieldMixin]:
|
131
118
|
for cfg_name, cfg_value in inspect.getmembers(
|
132
|
-
record_class, lambda x: isinstance(x,
|
119
|
+
record_class, lambda x: isinstance(x, MappingSystemFieldMixin)
|
133
120
|
):
|
134
|
-
yield cfg_value
|
121
|
+
yield cfg_value
|
oarepo_runtime/cli/index.py
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
import inspect
|
2
|
+
|
3
|
+
from invenio_records.dumpers import SearchDumperExt
|
4
|
+
|
5
|
+
|
6
|
+
class MappingSystemFieldMixin:
|
7
|
+
@property
|
8
|
+
def mapping(self):
|
9
|
+
return {}
|
10
|
+
|
11
|
+
@property
|
12
|
+
def mapping_settings(self):
|
13
|
+
return {}
|
14
|
+
|
15
|
+
|
16
|
+
class SystemFieldDumperExt(SearchDumperExt):
|
17
|
+
def dump(self, record, data):
|
18
|
+
"""Dump custom fields."""
|
19
|
+
for cf in inspect.getmembers(
|
20
|
+
record, lambda x: isinstance(x, MappingSystemFieldMixin)
|
21
|
+
):
|
22
|
+
cf[1].search_dump(data)
|
23
|
+
|
24
|
+
def load(self, data, record_cls):
|
25
|
+
"""Load custom fields."""
|
26
|
+
for cf in inspect.getmembers(
|
27
|
+
record_cls, lambda x: isinstance(x, MappingSystemFieldMixin)
|
28
|
+
):
|
29
|
+
cf[1].search_load(data)
|
@@ -0,0 +1,132 @@
|
|
1
|
+
from functools import cached_property
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
from flask import current_app
|
5
|
+
from invenio_records.systemfields import SystemField
|
6
|
+
|
7
|
+
from oarepo_runtime.records import MappingSystemFieldMixin
|
8
|
+
from oarepo_runtime.relations.lookup import lookup_key
|
9
|
+
|
10
|
+
|
11
|
+
class ICUField(SystemField):
|
12
|
+
"""
|
13
|
+
A system field that acts as an opensearch "proxy" to another field.
|
14
|
+
It creates a top-level mapping field with the same name and copies
|
15
|
+
content of {another field}.language into {mapping field}.language.
|
16
|
+
|
17
|
+
The language accessor can be modified by overriding get_values method.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, *, source_field, key=None):
|
21
|
+
super().__init__(key)
|
22
|
+
self.source_field = source_field
|
23
|
+
|
24
|
+
@cached_property
|
25
|
+
def languages(self) -> Dict[str, Dict]:
|
26
|
+
icu_languages = current_app.config.get("OAREPO_ICU_LANGUAGES", {})
|
27
|
+
if icu_languages:
|
28
|
+
return icu_languages
|
29
|
+
|
30
|
+
primary_language = current_app.config.get("BABEL_DEFAULT_LOCALE", "en")
|
31
|
+
# list of tuples [lang, title], just take lang
|
32
|
+
babel_languages = [x[0] for x in current_app.config.get("I18N_LANGUAGES", [])]
|
33
|
+
|
34
|
+
return {primary_language: {}, **{k: {} for k in babel_languages}}
|
35
|
+
|
36
|
+
def get_values(self, data, language):
|
37
|
+
ret = []
|
38
|
+
for l in lookup_key(data, f"{self.source_field}"):
|
39
|
+
if isinstance(l.value, str):
|
40
|
+
ret.append(l.value)
|
41
|
+
elif isinstance(l.value, dict):
|
42
|
+
val = l.value.get(language)
|
43
|
+
if val:
|
44
|
+
ret.append(val)
|
45
|
+
return ret
|
46
|
+
|
47
|
+
def search_dump(self, data):
|
48
|
+
ret = {}
|
49
|
+
for lang in self.languages:
|
50
|
+
ret[lang] = self.get_values(data, lang)
|
51
|
+
data[self.attr_name] = ret
|
52
|
+
|
53
|
+
def search_load(self, data):
|
54
|
+
data.pop(self.attr_name, None)
|
55
|
+
|
56
|
+
def __get__(self, instance, owner):
|
57
|
+
return self
|
58
|
+
|
59
|
+
|
60
|
+
class ICUSortField(MappingSystemFieldMixin, ICUField):
|
61
|
+
"""
|
62
|
+
A field that adds icu sorting field
|
63
|
+
"""
|
64
|
+
|
65
|
+
def __init__(self, *, source_field, key=None):
|
66
|
+
super().__init__(source_field=source_field, key=key)
|
67
|
+
|
68
|
+
@property
|
69
|
+
def mapping(self):
|
70
|
+
return {
|
71
|
+
self.attr_name: {
|
72
|
+
"type": "object",
|
73
|
+
"properties": {
|
74
|
+
lang: {
|
75
|
+
"type": "icu_collation_keyword",
|
76
|
+
"index": False,
|
77
|
+
"language": lang,
|
78
|
+
**setting.get("collation", {}),
|
79
|
+
}
|
80
|
+
for lang, setting in self.languages.items()
|
81
|
+
},
|
82
|
+
},
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
class ICUSuggestField(MappingSystemFieldMixin, ICUField):
|
87
|
+
"""
|
88
|
+
A field that adds icu-aware suggestion field
|
89
|
+
"""
|
90
|
+
|
91
|
+
def __init__(self, source_field, key=None):
|
92
|
+
super().__init__(source_field=source_field, key=key)
|
93
|
+
|
94
|
+
@property
|
95
|
+
def mapping(self):
|
96
|
+
return {
|
97
|
+
self.attr_name: {
|
98
|
+
"type": "object",
|
99
|
+
"properties": {
|
100
|
+
lang: setting.get(
|
101
|
+
"suggest",
|
102
|
+
{
|
103
|
+
"type": "text",
|
104
|
+
"fields": {
|
105
|
+
"original": {
|
106
|
+
"type": "search_as_you_type",
|
107
|
+
},
|
108
|
+
"no_accent": {
|
109
|
+
"type": "search_as_you_type",
|
110
|
+
"analyzer": "accent_removal_analyzer",
|
111
|
+
},
|
112
|
+
},
|
113
|
+
},
|
114
|
+
)
|
115
|
+
for lang, setting in self.languages.items()
|
116
|
+
},
|
117
|
+
},
|
118
|
+
}
|
119
|
+
|
120
|
+
@property
|
121
|
+
def mapping_settings(self):
|
122
|
+
return {
|
123
|
+
"analysis": {
|
124
|
+
"analyzer": {
|
125
|
+
"accent_removal_analyzer": {
|
126
|
+
"type": "custom",
|
127
|
+
"tokenizer": "standard",
|
128
|
+
"filter": ["lowercase", "asciifolding"],
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
@@ -12,5 +12,7 @@ class MultipleInvalidRelationErrors(Exception):
|
|
12
12
|
"""
|
13
13
|
|
14
14
|
def __init__(self, errors):
|
15
|
-
super().__init__(
|
15
|
+
super().__init__(
|
16
|
+
"; ".join([f"{e[0]}: {type(e[1]).__name__}({e[1]})" for e in errors])
|
17
|
+
)
|
16
18
|
self.errors = errors
|
@@ -0,0 +1,160 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import inspect
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
# TODO: integrate this to invenio_records_resources.services.records and remove SearchOptions class
|
6
|
+
from flask_babelex import lazy_gettext as _
|
7
|
+
from invenio_records_resources.proxies import current_service_registry
|
8
|
+
from invenio_records_resources.services.records import (
|
9
|
+
SearchOptions as InvenioSearchOptions,
|
10
|
+
)
|
11
|
+
from invenio_records_resources.services.records.queryparser import SuggestQueryParser
|
12
|
+
|
13
|
+
from oarepo_runtime.records.icu import ICUSuggestField
|
14
|
+
|
15
|
+
try:
|
16
|
+
from invenio_i18n import get_locale
|
17
|
+
except ImportError:
|
18
|
+
from invenio_i18n.babel import get_locale
|
19
|
+
|
20
|
+
|
21
|
+
class SearchOptions(InvenioSearchOptions):
|
22
|
+
sort_options = {
|
23
|
+
"title": dict(
|
24
|
+
title=_("By Title"),
|
25
|
+
fields=["metadata.title"], # ES defaults to desc on `_score` field
|
26
|
+
),
|
27
|
+
"bestmatch": dict(
|
28
|
+
title=_("Best match"),
|
29
|
+
fields=["_score"], # ES defaults to desc on `_score` field
|
30
|
+
),
|
31
|
+
"newest": dict(
|
32
|
+
title=_("Newest"),
|
33
|
+
fields=["-created"],
|
34
|
+
),
|
35
|
+
"oldest": dict(
|
36
|
+
title=_("Oldest"),
|
37
|
+
fields=["created"],
|
38
|
+
),
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
@dataclasses.dataclass
|
43
|
+
class SuggestField:
|
44
|
+
field: str
|
45
|
+
boost: int
|
46
|
+
use_ngrams: bool = True
|
47
|
+
boost_exact: float = 5
|
48
|
+
boost_2gram: float = 1
|
49
|
+
boost_3gram: float = 1
|
50
|
+
boost_prefix: float = 1
|
51
|
+
|
52
|
+
|
53
|
+
class ICUSuggestParser:
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
record_cls_or_service_name,
|
57
|
+
extra_fields: List[SuggestField] = None,
|
58
|
+
default_fields: List[SuggestField] = None,
|
59
|
+
):
|
60
|
+
self.record_cls_or_service_name = record_cls_or_service_name
|
61
|
+
self.extra_fields = extra_fields or []
|
62
|
+
self.default_fields = default_fields or []
|
63
|
+
|
64
|
+
@property
|
65
|
+
def record_cls(self):
|
66
|
+
if not isinstance(self.record_cls_or_service_name, str):
|
67
|
+
return self.record_cls_or_service_name
|
68
|
+
return current_service_registry.get(
|
69
|
+
self.record_cls_or_service_name
|
70
|
+
).config.record_cls
|
71
|
+
|
72
|
+
def __get__(self, instance, owner):
|
73
|
+
search_as_you_type_fields: List[SuggestField] = []
|
74
|
+
locale = get_locale()
|
75
|
+
if locale:
|
76
|
+
language = locale.language
|
77
|
+
|
78
|
+
for fld_name, fld in inspect.getmembers(
|
79
|
+
self.record_cls, lambda x: isinstance(x, ICUSuggestField)
|
80
|
+
):
|
81
|
+
search_as_you_type_fields.append(
|
82
|
+
SuggestField(f"{fld_name}.{language}.original", 2)
|
83
|
+
)
|
84
|
+
search_as_you_type_fields.append(
|
85
|
+
SuggestField(f"{fld_name}.{language}.no_accent", 1)
|
86
|
+
)
|
87
|
+
|
88
|
+
if not search_as_you_type_fields:
|
89
|
+
search_as_you_type_fields.extend(self.default_fields)
|
90
|
+
|
91
|
+
search_as_you_type_fields.extend(self.extra_fields)
|
92
|
+
|
93
|
+
fields = []
|
94
|
+
for fld in search_as_you_type_fields:
|
95
|
+
fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
|
96
|
+
if fld.use_ngrams:
|
97
|
+
fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
|
98
|
+
fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
|
99
|
+
fields.append(
|
100
|
+
f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
|
101
|
+
)
|
102
|
+
|
103
|
+
return SuggestQueryParser.factory(
|
104
|
+
fields=fields,
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
@dataclasses.dataclass
|
109
|
+
class SortField:
|
110
|
+
option_name: str = "title"
|
111
|
+
icu_sort_field: str = "sort"
|
112
|
+
title: str = "Title"
|
113
|
+
|
114
|
+
|
115
|
+
class ICUSortOptions:
|
116
|
+
def __init__(self, record_cls_or_service_name, fields: List[SortField] = None):
|
117
|
+
self.record_cls_or_service_name = record_cls_or_service_name
|
118
|
+
self.fields = fields or [SortField()]
|
119
|
+
|
120
|
+
@property
|
121
|
+
def record_cls(self):
|
122
|
+
if not isinstance(self.record_cls_or_service_name, str):
|
123
|
+
return self.record_cls_or_service_name
|
124
|
+
return current_service_registry.get(
|
125
|
+
self.record_cls_or_service_name
|
126
|
+
).config.record_cls
|
127
|
+
|
128
|
+
def __get__(self, instance, owner):
|
129
|
+
if not inspect.isclass(owner):
|
130
|
+
owner = type(owner)
|
131
|
+
super_options = {}
|
132
|
+
for mro in list(owner.mro())[1:]:
|
133
|
+
if hasattr(mro, "sort_options"):
|
134
|
+
super_options = mro.sort_options
|
135
|
+
break
|
136
|
+
|
137
|
+
ret = {
|
138
|
+
**super_options,
|
139
|
+
**getattr(owner, "extra_sort_options", {}),
|
140
|
+
}
|
141
|
+
|
142
|
+
# transform the sort options by the current language
|
143
|
+
locale = get_locale()
|
144
|
+
if not locale:
|
145
|
+
return ret
|
146
|
+
|
147
|
+
language = locale.language
|
148
|
+
|
149
|
+
for sort_field in self.fields:
|
150
|
+
icu_field = getattr(self.record_cls, sort_field.icu_sort_field)
|
151
|
+
ret[sort_field.option_name] = {
|
152
|
+
"title": sort_field.title,
|
153
|
+
"fields": [f"{icu_field.key}.{language}"],
|
154
|
+
}
|
155
|
+
return ret
|
156
|
+
|
157
|
+
|
158
|
+
class I18nSearchOptions(SearchOptions):
|
159
|
+
extra_sort_options = {}
|
160
|
+
record_cls = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: oarepo-runtime
|
3
|
-
Version: 1.4.
|
3
|
+
Version: 1.4.13
|
4
4
|
Summary: A set of runtime extensions of Invenio repository
|
5
5
|
Description-Content-Type: text/markdown
|
6
6
|
License-File: LICENSE
|
@@ -101,3 +101,42 @@ This module provides a marshmallow validator for date strings.
|
|
101
101
|
|
102
102
|
Provides interface and definitions for loading
|
103
103
|
preconfigured permission sets to service config.
|
104
|
+
|
105
|
+
## ICU sort and suggestions
|
106
|
+
|
107
|
+
To use ICU sort and suggestion custom fields, provide the following configuration
|
108
|
+
to `oarepo-model-builder` (or put this stuff to your custom superclasses).
|
109
|
+
|
110
|
+
```yaml
|
111
|
+
record:
|
112
|
+
imports:
|
113
|
+
- import: invenio_records_resources.records.api.Record
|
114
|
+
alias: InvenioRecord
|
115
|
+
- import: oarepo_runtime.records.SystemFieldDumperExt
|
116
|
+
- import: oarepo_runtime.records.icu.ICUSortField
|
117
|
+
- import: oarepo_runtime.records.icu.ICUSuggestField
|
118
|
+
extra-code: |-2
|
119
|
+
# extra custom fields for testing ICU sorting and suggesting
|
120
|
+
sort = ICUSortField(source_field="metadata.title")
|
121
|
+
suggest = ICUSuggestField(source_field="metadata.title")
|
122
|
+
search-options:
|
123
|
+
base-classes:
|
124
|
+
- I18nSearchOptions
|
125
|
+
imports:
|
126
|
+
- import: oarepo_runtime.services.icu.I18nSearchOptions
|
127
|
+
- import: oarepo_runtime.services.icu.ICUSuggestParser
|
128
|
+
- import: oarepo_runtime.services.icu.ICUSortOptions
|
129
|
+
sort-options-field: extra_sort_options
|
130
|
+
extra-code: |-2
|
131
|
+
suggest_parser_cls = ICUSuggestParser("records2")
|
132
|
+
sort_options = ICUSortOptions("records2")
|
133
|
+
|
134
|
+
record-dumper:
|
135
|
+
extensions:
|
136
|
+
- SystemFieldDumperExt()
|
137
|
+
```
|
138
|
+
|
139
|
+
Run `invenio oarepo cf init` to initialize custom fields,
|
140
|
+
`invenio oarepo index reindex` if you already have data
|
141
|
+
inside the repository and from this moment on,
|
142
|
+
`/records?sort=title` and `/records?suggest=abc` should work
|
@@ -5,15 +5,14 @@ oarepo_runtime/marshmallow.py,sha256=BaRh9Z07h2DznYMyYxiTmSfe4EJaeXvTY8lKVyvVGa4
|
|
5
5
|
oarepo_runtime/polymorphic.py,sha256=CkvXVUiXbrsLWFgoNnjjpUviQyzRMCmpsD3GWfV0WZA,494
|
6
6
|
oarepo_runtime/profile.py,sha256=QzrQoZncjoN74ZZnpkEKakNk08KCzBU7m6y42RN8AMY,1637
|
7
7
|
oarepo_runtime/uow.py,sha256=TqVYF1N24WTVIjf97mlb-EH-BtIZCVVLNS4bqZt4XLs,3201
|
8
|
-
oarepo_runtime/cf/__init__.py,sha256
|
9
|
-
oarepo_runtime/cf/cli.py,sha256=
|
10
|
-
oarepo_runtime/cf/
|
11
|
-
oarepo_runtime/cf/mappings.py,sha256=uCLyMEchJPtQLR5jUQKZmuZkWAMRXLk5JnNOH30E3Y0,4901
|
8
|
+
oarepo_runtime/cf/__init__.py,sha256=-YhE1JnHrX-qS_IT_1Lh5_OQ2OMbErQyZ3mPxLbiYqw,2241
|
9
|
+
oarepo_runtime/cf/cli.py,sha256=gUD4tRV57ixpgQxECN-eporJnC9etBfHE-FNKEOUUsY,316
|
10
|
+
oarepo_runtime/cf/mappings.py,sha256=6KKiS93tMPZOcmdBBvfXh3VOAh5Kw8xdXh2ER2PP0Xo,4200
|
12
11
|
oarepo_runtime/cli/__init__.py,sha256=-WGXmjHoSqiApR_LvYnZTimuL-frR7SynrsSklnjb3A,221
|
13
12
|
oarepo_runtime/cli/assets.py,sha256=XLZTnsGb88O5N8R2D3AYpZqtnO4JrbybUtRLKnL1p3w,2430
|
14
13
|
oarepo_runtime/cli/base.py,sha256=xZMsR2rati5Mz0DZzmnlhVI7E6ePCfnOiTayrxT9cWU,259
|
15
14
|
oarepo_runtime/cli/check.py,sha256=zQ6txhfN6LWvdWgRa3ZFdOifbuazoRQd-7ml0qwIBWg,2358
|
16
|
-
oarepo_runtime/cli/index.py,sha256=
|
15
|
+
oarepo_runtime/cli/index.py,sha256=bZHxsJDzCv96_6n27lmLE9ssTgx3NViq9lTeRUJBv-4,4684
|
17
16
|
oarepo_runtime/cli/validate.py,sha256=HpSvHQCGHlrdgdpKix9cIlzlBoJEiT1vACZdMnOUGEY,2827
|
18
17
|
oarepo_runtime/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
18
|
oarepo_runtime/config/permissions_presets.py,sha256=zApeA-2DYAlD--SzVz3vq_OFjq48Ko0pe08e4o2vxr4,6114
|
@@ -58,10 +57,12 @@ oarepo_runtime/i18n/dumper.py,sha256=PbNFCLsiH4XV3E1v8xga_fzlcEImHy8OXn_UKh_8VBU
|
|
58
57
|
oarepo_runtime/i18n/schema.py,sha256=wJLyE4Ew4mqEGoMpM6JOsyyqGCV_R4YgWQUm45AHVPU,1184
|
59
58
|
oarepo_runtime/i18n/ui_schema.py,sha256=_iMUkHI8fPQT9hJziSMqYRyGmozgJrY5qQo1Y7Fmuo4,2085
|
60
59
|
oarepo_runtime/i18n/validation.py,sha256=fyMTi2Rw-KiHv7c7HN61zGxRVa9sAjAEEkAL5wUyKNo,236
|
60
|
+
oarepo_runtime/records/__init__.py,sha256=jzKWn4jaVKZvysPZHtqWE_SWExh_4DBz7YSgflFyYoM,721
|
61
|
+
oarepo_runtime/records/icu.py,sha256=h8RkqAO8d0LOqj27IVa-B-Bqs08na0lffqczJZHTgUU,4137
|
61
62
|
oarepo_runtime/relations/__init__.py,sha256=bDAgxl_LdKsqpGG3qluxAkQnn5u2ItJngnHQKkqzlkE,373
|
62
63
|
oarepo_runtime/relations/base.py,sha256=I5XANA-fFbiH3xQ1s7x1NVJFaiSRKCc6-xyNy1LEfpw,8739
|
63
64
|
oarepo_runtime/relations/components.py,sha256=J9rvzaAoRDbSVuA01hIOlXKQP-OE5VJI5w5xuMsFO70,602
|
64
|
-
oarepo_runtime/relations/errors.py,sha256=
|
65
|
+
oarepo_runtime/relations/errors.py,sha256=VtlOKq9MEUeJ4IsiZhY7lWoshrusA_RL4SOHe2titno,552
|
65
66
|
oarepo_runtime/relations/internal.py,sha256=OTp8iJqyl80sWDk0Q0AK42l6UsxZDABspVU_GwWza9o,1556
|
66
67
|
oarepo_runtime/relations/lookup.py,sha256=wi3jPfOedazOmhOMrgu50PUETc1jfSdpmjK0wvOFsEM,848
|
67
68
|
oarepo_runtime/relations/mapping.py,sha256=jwFCWCnW8hb44ZZBeVf96QR79S9FT2hvm4L8EuCBo5U,1277
|
@@ -70,7 +71,7 @@ oarepo_runtime/relations/uow.py,sha256=KrN8B-wVbmb0kOErxb7bAhPmOR6-mMRgBr-ab-ir6
|
|
70
71
|
oarepo_runtime/resolvers/__init__.py,sha256=kTlvSiympib59YQV7wEKpIXGprPWRuvxLIwmeeQdUec,89
|
71
72
|
oarepo_runtime/resolvers/proxies.py,sha256=egtT7uXL91KswWI7gqUIaz1vWIHezdsiI_M-xRKXWww,547
|
72
73
|
oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
73
|
-
oarepo_runtime/services/
|
74
|
+
oarepo_runtime/services/icu.py,sha256=5uxCHEBIEBlKo8IO4y_GA6InYVPAq6sPmWXWeBOXLA8,4969
|
74
75
|
oarepo_runtime/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
75
76
|
oarepo_runtime/tasks/datastreams.py,sha256=_EJZuHg2UoIJCrrOo0J7e8uCSoXIpJsRJnsb8sorWJc,10810
|
76
77
|
oarepo_runtime/translations/messages.pot,sha256=NfZakWEYMZE_6HvnKxO7B61UnHnWgtKSvSKMJQ6msK0,971
|
@@ -84,9 +85,9 @@ oarepo_runtime/utils/path.py,sha256=V1NVyk3m12_YLbj7QHYvUpE1wScO78bYsX1LOLeXDkI,
|
|
84
85
|
oarepo_runtime/validation/__init__.py,sha256=lU7DgZq8pGD5Pa-QqL9gvLsib3IYtM-Y56k-NwHrPG0,166
|
85
86
|
oarepo_runtime/validation/dates.py,sha256=fahqKGDdIYWux5ZeoljrEe8VD2fDZR9VpfvYmTYAmpw,1050
|
86
87
|
tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
87
|
-
oarepo_runtime-1.4.
|
88
|
-
oarepo_runtime-1.4.
|
89
|
-
oarepo_runtime-1.4.
|
90
|
-
oarepo_runtime-1.4.
|
91
|
-
oarepo_runtime-1.4.
|
92
|
-
oarepo_runtime-1.4.
|
88
|
+
oarepo_runtime-1.4.13.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
|
89
|
+
oarepo_runtime-1.4.13.dist-info/METADATA,sha256=qQgSqdIgdpLdcP8i5I8j9B3b_bhyHLdJGUxZGYu4u3Y,4288
|
90
|
+
oarepo_runtime-1.4.13.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
91
|
+
oarepo_runtime-1.4.13.dist-info/entry_points.txt,sha256=C32W4eT-8OypMCfwOO5WREioVKSneDfY51D78Uvdbp0,231
|
92
|
+
oarepo_runtime-1.4.13.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
|
93
|
+
oarepo_runtime-1.4.13.dist-info/RECORD,,
|
oarepo_runtime/cf/icu.py
DELETED
@@ -1,92 +0,0 @@
|
|
1
|
-
from invenio_records_resources.services.custom_fields import BaseCF
|
2
|
-
|
3
|
-
from oarepo_runtime.relations.lookup import lookup_key
|
4
|
-
|
5
|
-
|
6
|
-
class ICUSortCF(BaseCF):
|
7
|
-
def __init__(
|
8
|
-
self, name, language, source_field, country=None, variant=None, field_args=None
|
9
|
-
):
|
10
|
-
super().__init__(name=name, field_args=field_args)
|
11
|
-
self.language = language
|
12
|
-
self.country = country
|
13
|
-
self.variant = variant
|
14
|
-
self.source_field = source_field
|
15
|
-
|
16
|
-
@property
|
17
|
-
def mapping(self):
|
18
|
-
ret = {
|
19
|
-
"type": "icu_collation_keyword",
|
20
|
-
"index": False,
|
21
|
-
"language": self.language,
|
22
|
-
}
|
23
|
-
if self.country:
|
24
|
-
ret["country"] = self.country
|
25
|
-
|
26
|
-
if self.variant:
|
27
|
-
ret["variant"] = self.variant
|
28
|
-
|
29
|
-
return ret
|
30
|
-
|
31
|
-
@property
|
32
|
-
def field(self):
|
33
|
-
return None
|
34
|
-
|
35
|
-
def dump(self, record, cf_key="custom_fields"):
|
36
|
-
ret = []
|
37
|
-
for l in lookup_key(record, self.source_field):
|
38
|
-
ret.append(l.value)
|
39
|
-
record.setdefault(cf_key, {})[self.name] = ret
|
40
|
-
|
41
|
-
def load(self, record, cf_key="custom_fields"):
|
42
|
-
record.pop(cf_key, None)
|
43
|
-
|
44
|
-
|
45
|
-
class ICUSuggestCF(BaseCF):
|
46
|
-
def __init__(self, name, language, source_field, field_args=None):
|
47
|
-
super().__init__(name=name, field_args=field_args)
|
48
|
-
self.language = language
|
49
|
-
self.source_field = source_field
|
50
|
-
|
51
|
-
@property
|
52
|
-
def mapping(self):
|
53
|
-
ret = {
|
54
|
-
"type": "text",
|
55
|
-
"fields": {
|
56
|
-
"original": {
|
57
|
-
"type": "search_as_you_type",
|
58
|
-
},
|
59
|
-
"no_accent": {
|
60
|
-
"type": "search_as_you_type",
|
61
|
-
"analyzer": "accent_removal_analyzer",
|
62
|
-
},
|
63
|
-
},
|
64
|
-
}
|
65
|
-
return ret
|
66
|
-
|
67
|
-
@property
|
68
|
-
def mapping_settings(self):
|
69
|
-
return {
|
70
|
-
"analysis": {
|
71
|
-
"analyzer": {
|
72
|
-
"accent_removal_analyzer": {
|
73
|
-
"type": "custom",
|
74
|
-
"tokenizer": "standard",
|
75
|
-
"filter": ["lowercase", "asciifolding"],
|
76
|
-
}
|
77
|
-
}
|
78
|
-
}
|
79
|
-
}
|
80
|
-
|
81
|
-
@property
|
82
|
-
def field(self):
|
83
|
-
return None
|
84
|
-
|
85
|
-
def dump(self, record, cf_key="custom_fields"):
|
86
|
-
ret = []
|
87
|
-
for l in lookup_key(record, self.source_field):
|
88
|
-
ret.append(l.value)
|
89
|
-
record.setdefault(cf_key, {})[self.name] = ret
|
90
|
-
|
91
|
-
def load(self, record, cf_key="custom_fields"):
|
92
|
-
record.pop(cf_key, None)
|
@@ -1,108 +0,0 @@
|
|
1
|
-
import dataclasses
|
2
|
-
from typing import Tuple, List
|
3
|
-
|
4
|
-
from invenio_records_resources.services.records import (
|
5
|
-
SearchOptions as InvenioSearchOptions,
|
6
|
-
)
|
7
|
-
|
8
|
-
# TODO: integrate this to invenio_records_resources.services.records
|
9
|
-
from flask_babelex import lazy_gettext as _
|
10
|
-
from invenio_records_resources.services.records.queryparser import SuggestQueryParser
|
11
|
-
from sqlalchemy.util import classproperty
|
12
|
-
from flask import current_app
|
13
|
-
|
14
|
-
try:
|
15
|
-
from invenio_i18n import get_locale
|
16
|
-
except ImportError:
|
17
|
-
from invenio_i18n.babel import get_locale
|
18
|
-
|
19
|
-
|
20
|
-
class SearchOptions(InvenioSearchOptions):
|
21
|
-
sort_options = {
|
22
|
-
"title": dict(
|
23
|
-
title=_("By Title"),
|
24
|
-
fields=["metadata.title"], # ES defaults to desc on `_score` field
|
25
|
-
),
|
26
|
-
"bestmatch": dict(
|
27
|
-
title=_("Best match"),
|
28
|
-
fields=["_score"], # ES defaults to desc on `_score` field
|
29
|
-
),
|
30
|
-
"newest": dict(
|
31
|
-
title=_("Newest"),
|
32
|
-
fields=["-created"],
|
33
|
-
),
|
34
|
-
"oldest": dict(
|
35
|
-
title=_("Oldest"),
|
36
|
-
fields=["created"],
|
37
|
-
),
|
38
|
-
}
|
39
|
-
|
40
|
-
|
41
|
-
@dataclasses.dataclass
|
42
|
-
class SuggestField:
|
43
|
-
field: str
|
44
|
-
boost: int
|
45
|
-
use_ngrams: bool = True
|
46
|
-
boost_exact: float = 5
|
47
|
-
boost_2gram: float = 1
|
48
|
-
boost_3gram: float = 1
|
49
|
-
boost_prefix: float = 1
|
50
|
-
|
51
|
-
|
52
|
-
class I18nSearchOptions(SearchOptions):
|
53
|
-
SORT_CUSTOM_FIELD_NAME = None
|
54
|
-
SUGGEST_CUSTOM_FIELD_NAME = None
|
55
|
-
SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS: Tuple[SuggestField] = (
|
56
|
-
SuggestField("metadata.title", 1, use_ngrams=False),
|
57
|
-
SuggestField("id", 1, use_ngrams=False),
|
58
|
-
)
|
59
|
-
extra_sort_options = {}
|
60
|
-
|
61
|
-
@classproperty
|
62
|
-
def sort_options(clz):
|
63
|
-
ret = {**super().sort_options, **clz.extra_sort_options}
|
64
|
-
if not clz.SORT_CUSTOM_FIELD_NAME:
|
65
|
-
return ret
|
66
|
-
|
67
|
-
# transform the sort options by the current language
|
68
|
-
locale = get_locale()
|
69
|
-
if not locale:
|
70
|
-
return ret
|
71
|
-
language = locale.language
|
72
|
-
for cf in current_app.config[clz.SORT_CUSTOM_FIELD_NAME]:
|
73
|
-
if cf.name == language:
|
74
|
-
ret["title"]["fields"] = [f"sort.{cf.name}"]
|
75
|
-
break
|
76
|
-
return ret
|
77
|
-
|
78
|
-
@classproperty
|
79
|
-
def suggest_parser_cls(clz):
|
80
|
-
search_as_you_type_fields: List[SuggestField] = []
|
81
|
-
locale = get_locale()
|
82
|
-
|
83
|
-
if clz.SUGGEST_CUSTOM_FIELD_NAME and locale:
|
84
|
-
language = locale.language
|
85
|
-
for cf in current_app.config[clz.SUGGEST_CUSTOM_FIELD_NAME]:
|
86
|
-
if cf.name == language:
|
87
|
-
search_as_you_type_fields.append(
|
88
|
-
SuggestField(f"suggest.{language}.original", 2)
|
89
|
-
)
|
90
|
-
search_as_you_type_fields.append(
|
91
|
-
SuggestField(f"suggest.{language}.no_accent", 1)
|
92
|
-
)
|
93
|
-
if not search_as_you_type_fields:
|
94
|
-
search_as_you_type_fields = clz.SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS # noqa
|
95
|
-
|
96
|
-
fields = []
|
97
|
-
for fld in search_as_you_type_fields:
|
98
|
-
fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
|
99
|
-
if fld.use_ngrams:
|
100
|
-
fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
|
101
|
-
fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
|
102
|
-
fields.append(
|
103
|
-
f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
|
104
|
-
)
|
105
|
-
|
106
|
-
return SuggestQueryParser.factory(
|
107
|
-
fields=fields,
|
108
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|