crossref-matcher 1.3.2__tar.gz → 1.4.0__tar.gz
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.
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/MANIFEST.in +1 -0
- {crossref_matcher-1.3.2/crossref_matcher.egg-info → crossref_matcher-1.4.0}/PKG-INFO +1 -1
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/README.md +27 -3
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/MatchTask.py +1 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/app.py +17 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/all_links.py +26 -2
- crossref_matcher-1.4.0/crossref_matcher/indexes/aliased_index.py +278 -0
- crossref_matcher-1.4.0/crossref_matcher/indexes/grants/create_index.py +115 -0
- crossref_matcher-1.4.0/crossref_matcher/indexes/grants/index_data.py +445 -0
- crossref_matcher-1.4.0/crossref_matcher/indexes/grants/normalize.py +97 -0
- crossref_matcher-1.4.0/crossref_matcher/indexes/meta.py +21 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/create_index.py +57 -12
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/index_data.py +192 -36
- crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations/update.py +137 -0
- crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations/validate.py +190 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/schemas.py +7 -0
- crossref_matcher-1.4.0/crossref_matcher/resources/VERSION.txt +1 -0
- crossref_matcher-1.4.0/crossref_matcher/resources/data/funders/ror_funder_mapping.csv.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/plugins.json +2 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/__init__.py +11 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +9 -2
- crossref_matcher-1.4.0/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +1 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/funder_ror.py +50 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/strategy.py +309 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/version.txt +1 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/reference/parsed_isbn/__init__.py +0 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv/__init__.py +0 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv_orig/__init__.py +0 -0
- crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0/crossref_matcher.egg-info}/PKG-INFO +1 -1
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/SOURCES.txt +15 -0
- crossref_matcher-1.3.2/crossref_matcher/resources/VERSION.txt +0 -1
- crossref_matcher-1.3.2/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +0 -1
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/LICENSE +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/api_routes.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/evaluation.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/ror_utils.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/schemas.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/sparse_metrics_verification/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/indexes/preprint_matching → crossref_matcher-1.4.0/crossref_matcher/indexes/grants}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations → crossref_matcher-1.4.0/crossref_matcher/indexes/preprint_matching}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/matching → crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/opensearch_utils.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources → crossref_matcher-1.4.0/crossref_matcher/matching}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/http_utils.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/search.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/utils.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/affiliation → crossref_matcher-1.4.0/crossref_matcher/resources}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data → crossref_matcher-1.4.0/crossref_matcher/resources/affiliation}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/common_ror_endings → crossref_matcher-1.4.0/crossref_matcher/resources/data}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/countries → crossref_matcher-1.4.0/crossref_matcher/resources/data/common_ror_endings}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/datasets → crossref_matcher-1.4.0/crossref_matcher/resources/data/countries}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/countries.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/models → crossref_matcher-1.4.0/crossref_matcher/resources/data/datasets}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/models/preprint_ensemble_ml → crossref_matcher-1.4.0/crossref_matcher/resources/data/funders}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/resources/data/models/reference_nn_unstructured → crossref_matcher-1.4.0/crossref_matcher/resources/data/models}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation → crossref_matcher-1.4.0/crossref_matcher/resources/data/models/preprint_ensemble_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/multi_search → crossref_matcher-1.4.0/crossref_matcher/resources/data/models/reference_nn_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/run_plugin_test.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/single_search → crossref_matcher-1.4.0/crossref_matcher/strategies/affiliation}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/matching.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/funder → crossref_matcher-1.4.0/crossref_matcher/strategies/affiliation/multi_search}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/funder/funder_name_to_ror_search → crossref_matcher-1.4.0/crossref_matcher/strategies/affiliation/single_search}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/common.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/healthcheck → crossref_matcher-1.4.0/crossref_matcher/strategies/funder}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/healthcheck/healthcheck → crossref_matcher-1.4.0/crossref_matcher/strategies/funder/funder_name_to_ror_search}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint → crossref_matcher-1.4.0/crossref_matcher/strategies/grant}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.4.0/crossref_matcher/strategies/healthcheck}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_ml → crossref_matcher-1.4.0/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/ensemble_avg}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv_es → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/ensemble_cascade}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/sbmv_rels → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/ensemble_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/sbmv}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/nn_unstructured → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/sbmv_es}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/parsed_doi → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.4.0/crossref_matcher/strategies/reference}/__init__.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv → crossref_matcher-1.4.0/crossref_matcher/strategies/reference/cs_combined}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv_orig → crossref_matcher-1.4.0/crossref_matcher/strategies/reference/nn_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
- {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/sbmv_unstructured → crossref_matcher-1.4.0/crossref_matcher/strategies/reference/parsed_doi}/__init__.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/versioning.py +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/dependency_links.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/entry_points.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/requires.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/top_level.txt +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/setup.cfg +0 -0
- {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/setup.py +0 -0
|
@@ -4,3 +4,4 @@ recursive-include crossref_matcher/strategies *.txt
|
|
|
4
4
|
recursive-include crossref_matcher/resources/data/models *.pkl *.gz *.local
|
|
5
5
|
recursive-include crossref_matcher/resources/data/common_ror_endings *.txt
|
|
6
6
|
recursive-include crossref_matcher/resources/data/countries *.txt
|
|
7
|
+
recursive-include crossref_matcher/resources/data/funders *.csv.gz
|
|
@@ -22,6 +22,8 @@ Looking for the source code to the matching strategies?
|
|
|
22
22
|
- Funder Name to ROR Search strategy -- [Source Code](crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py)
|
|
23
23
|
- Affiliation matching
|
|
24
24
|
- Single Search strategy -- [Source Code](crossref_matcher/strategies/affiliation/single_search/strategy.py)
|
|
25
|
+
- Preprint matching
|
|
26
|
+
- SBMV (Search-Based Matching with Validation) -- [Source Code](crossref_matcher/strategies/preprint/sbmv/strategy.py)
|
|
25
27
|
|
|
26
28
|
## Terminology
|
|
27
29
|
|
|
@@ -119,6 +121,22 @@ Some strategies need an OpenSearch index to match. [indexes directory](crossref_
|
|
|
119
121
|
|
|
120
122
|
Use ES_HOST env var to point Marple to the OpenSearch cluster.
|
|
121
123
|
|
|
124
|
+
#### Bootstrapping indexes for integration testing
|
|
125
|
+
|
|
126
|
+
The `organization` (ROR) and `grant` indexes can each be created and populated from a pinned, static dataset hosted in a public Crossref S3 bucket. This needs no AWS credentials and no local data files, so it works from inside a bare container — it is how the [integration harness](https://gitlab.com/crossref/integration) populates OpenSearch on startup:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
python -m crossref_matcher.indexes.ror_organizations.create_index
|
|
130
|
+
python -m crossref_matcher.indexes.ror_organizations.index_data --integration
|
|
131
|
+
|
|
132
|
+
python -m crossref_matcher.indexes.grants.create_index
|
|
133
|
+
python -m crossref_matcher.indexes.grants.index_data --integration
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`ror_organizations.create_index` creates a dated index (`organization-local-<time>`) behind the `organization` alias, and `index_data --integration` loads it through the alias. The `update_ror_index` DAG builds each new ROR version into its own dated index behind the same alias; see [`ror_organizations/update.py`](crossref_matcher/indexes/ror_organizations/update.py). To rebuild at the current ROR version (for example, after adding a document field), trigger the DAG with its `force` param, or run `index_data --force`.
|
|
137
|
+
|
|
138
|
+
The grants dataset is a pre-built export of the finished index documents, so the `--integration` path needs no source tables, no ROR dump and no `pyarrow`. Regenerate and republish it with [`scripts/export_grant_index_docs.py`](scripts/export_grant_index_docs.py); see [data/grants_awards/README.md](data/grants_awards/README.md).
|
|
139
|
+
|
|
122
140
|
### How to run
|
|
123
141
|
|
|
124
142
|
Run
|
|
@@ -178,6 +196,12 @@ To enable the check locally (as a non-blocking warning):
|
|
|
178
196
|
git config core.hooksPath .githooks
|
|
179
197
|
```
|
|
180
198
|
|
|
199
|
+
### Updating a strategy in production
|
|
200
|
+
|
|
201
|
+
For the step-by-step process of shipping a change to an existing strategy
|
|
202
|
+
(editing the strategy code, bumping its version, and merging to `main`), see
|
|
203
|
+
[how_to_update_a_strategy.md](crossref_matcher/strategies/how_to_update_a_strategy.md).
|
|
204
|
+
|
|
181
205
|
### Dependencies
|
|
182
206
|
|
|
183
207
|
Requirements are defined in `setup.py`. Requirements files with exact versions can be updated with `pip-compile`, which is a part of `pip-tools`. First run `pip install pip-tools`. Then:
|
|
@@ -192,7 +216,7 @@ CI (see [.gitlab-ci.yml](.gitlab-ci.yml)) deploys from two long-lived branches:
|
|
|
192
216
|
|
|
193
217
|
| Branch / trigger | Deploys to | Jobs |
|
|
194
218
|
|---|---|---|
|
|
195
|
-
| `dev` | **Research** environment | ECS deploy (`marple-research`), Airflow DAG data sync to `
|
|
219
|
+
| `dev` | **Research** environment | ECS deploy (`marple-research`), Airflow DAG data sync to the research Airflow DAG bucket (set by the `research`-scoped `AIRFLOW_DAG_BUCKET` CI/CD variable in GitLab; the research DAG runs on the Airflow 3 MWAA environment), `X.Y.Z-dev.N` prerelease tags |
|
|
196
220
|
| `main` | **Staging** environment | ECS deploy (`marple-staging`), GitLab-registry image for the integration project, release tags via semantic-release |
|
|
197
221
|
| Stable `vX.Y.Z` tags | PyPI | `crossref-matcher` published via OIDC trusted publishing (prerelease tags are not published) |
|
|
198
222
|
|
|
@@ -276,5 +300,5 @@ features still in flight:
|
|
|
276
300
|
4. Disable force-push again.
|
|
277
301
|
|
|
278
302
|
The reset push re-deploys research and re-syncs the Airflow DAG data in
|
|
279
|
-
|
|
280
|
-
files added by abandoned experiments are removed, not just left stale).
|
|
303
|
+
the research Airflow DAG bucket to the reset state (the sync uses `--delete`,
|
|
304
|
+
so files added by abandoned experiments are removed, not just left stale).
|
|
@@ -10,6 +10,7 @@ class MatchTask(Enum):
|
|
|
10
10
|
PREPRINT = ("preprint", "Matching journal articles to preprints.")
|
|
11
11
|
AFFILIATION = ("affiliation", "Matching affiliations to ROR IDs.")
|
|
12
12
|
FUNDER = ("funder", "Matching funders to ROR IDs.")
|
|
13
|
+
GRANT = ("grant", "Matching grant awards to DOIs.")
|
|
13
14
|
OTHER = ("other", "A generic matching task.")
|
|
14
15
|
HEALTHCHECK = (
|
|
15
16
|
"healthcheck",
|
|
@@ -9,6 +9,7 @@ from crossref_matcher import MatchTask, __version__
|
|
|
9
9
|
from crossref_matcher.evaluation.api_routes import router as evaluation_router
|
|
10
10
|
from crossref_matcher.matching import schemas
|
|
11
11
|
from crossref_matcher.strategies import (
|
|
12
|
+
InvalidInputError,
|
|
12
13
|
Strategy,
|
|
13
14
|
get_default_strategy,
|
|
14
15
|
list_strategies,
|
|
@@ -196,6 +197,8 @@ def match(
|
|
|
196
197
|
items=items, strategy=strategy_id, target_data=target_data
|
|
197
198
|
)
|
|
198
199
|
)
|
|
200
|
+
except InvalidInputError as e:
|
|
201
|
+
raise HTTPException(status_code=422, detail=str(e))
|
|
199
202
|
except NotImplementedError:
|
|
200
203
|
raise HTTPException(
|
|
201
204
|
status_code=400,
|
|
@@ -203,9 +206,22 @@ def match(
|
|
|
203
206
|
)
|
|
204
207
|
|
|
205
208
|
|
|
209
|
+
# Replaces FastAPI's own 422 entry, whose schema covers only parameter
|
|
210
|
+
# validation errors and not the message an InvalidInputError carries.
|
|
211
|
+
MATCH_RESPONSES = {
|
|
212
|
+
422: {
|
|
213
|
+
"model": schemas.ErrorResponse,
|
|
214
|
+
"description": "A parameter is missing or malformed, or the strategy "
|
|
215
|
+
"can't use the input. For the input, `detail` is a message describing "
|
|
216
|
+
"what the strategy expects.",
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
206
221
|
@app.get(
|
|
207
222
|
"/match",
|
|
208
223
|
response_model=schemas.MatchedItemsResponse,
|
|
224
|
+
responses=MATCH_RESPONSES,
|
|
209
225
|
tags=["Matching"],
|
|
210
226
|
description="Match input to identifiers.",
|
|
211
227
|
response_class=AsciiJSONResponse,
|
|
@@ -221,6 +237,7 @@ async def match_get(
|
|
|
221
237
|
@app.post(
|
|
222
238
|
"/match",
|
|
223
239
|
response_model=schemas.MatchedItemsResponse,
|
|
240
|
+
responses=MATCH_RESPONSES,
|
|
224
241
|
tags=["Matching"],
|
|
225
242
|
description="Match input to identifiers.",
|
|
226
243
|
response_class=AsciiJSONResponse,
|
|
@@ -126,7 +126,23 @@ def get_all_links(items: Iterable[dict]) -> list[dict]:
|
|
|
126
126
|
if len(item["output"]) == 0:
|
|
127
127
|
this_result_true = ["no_match"]
|
|
128
128
|
else:
|
|
129
|
-
this_result_true = item["output"]
|
|
129
|
+
this_result_true = list(item["output"])
|
|
130
|
+
|
|
131
|
+
# `matched` (and its relaxed/alternate counterpart) can contain more
|
|
132
|
+
# entries than there are true links, e.g. a strategy that returns
|
|
133
|
+
# several candidate matches for a single expected link.
|
|
134
|
+
# construct_matched_list only pairs up to len(this_result_true)
|
|
135
|
+
# matches and silently drops the rest, so pad the true list with
|
|
136
|
+
# "no_match" placeholders to give every matched entry its own row
|
|
137
|
+
# (which then correctly resolves to FP via res_cat).
|
|
138
|
+
n_extra = max(
|
|
139
|
+
0,
|
|
140
|
+
len(item["matched"]) - len(this_result_true),
|
|
141
|
+
len(get_matched_alternate(item)) - len(this_result_true),
|
|
142
|
+
)
|
|
143
|
+
if n_extra:
|
|
144
|
+
this_result_true = this_result_true + ["no_match"] * n_extra
|
|
145
|
+
|
|
130
146
|
alternates_negative = [
|
|
131
147
|
x[1] for x in item.get("alternates", []) if x[0] == "no_match"
|
|
132
148
|
]
|
|
@@ -139,6 +155,7 @@ def get_all_links(items: Iterable[dict]) -> list[dict]:
|
|
|
139
155
|
get_matched_alternate(item),
|
|
140
156
|
alternates_negative=alternates_negative,
|
|
141
157
|
)
|
|
158
|
+
no_match_count = 0
|
|
142
159
|
for i, this_row_true in enumerate(this_result_true):
|
|
143
160
|
if this_row_true in this_result_matched:
|
|
144
161
|
this_row_matched = this_row_true
|
|
@@ -150,7 +167,14 @@ def get_all_links(items: Iterable[dict]) -> list[dict]:
|
|
|
150
167
|
else:
|
|
151
168
|
this_row_matched_relaxed = this_result_matched_relaxed[0]
|
|
152
169
|
this_result_matched_relaxed.remove(this_row_matched_relaxed)
|
|
153
|
-
|
|
170
|
+
if this_row_true == "no_match":
|
|
171
|
+
# Multiple padded/no-output rows can share the same item, so
|
|
172
|
+
# disambiguate beyond the first to keep unique_id unique.
|
|
173
|
+
suffix = f"_{no_match_count}" if no_match_count else ""
|
|
174
|
+
unique_id = f"""{item["seq_no"]:06}_no_match{suffix}"""
|
|
175
|
+
no_match_count += 1
|
|
176
|
+
else:
|
|
177
|
+
unique_id = f"""{item["seq_no"]:06}_{this_row_true}"""
|
|
154
178
|
target_in_initial_candidates = get_candidate_position(
|
|
155
179
|
item["extra"].get("initial_candidates", None), this_row_true
|
|
156
180
|
)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Build an OpenSearch index under a dated name and serve it through an alias.
|
|
2
|
+
|
|
3
|
+
Each build creates ``<alias>-<suffix>``, loads and validates it, then moves
|
|
4
|
+
the alias onto it in one atomic ``_aliases`` call, so readers and writers
|
|
5
|
+
using the alias name never see a half-built index. Older dated indexes are
|
|
6
|
+
pruned, keeping a few for rollback.
|
|
7
|
+
|
|
8
|
+
If a concrete index (not an alias) still sits under the alias name, the
|
|
9
|
+
build migrates it: the concrete index is cloned to a dated rollback copy,
|
|
10
|
+
and the swap removes it and adds the alias in the same ``_aliases`` call, so
|
|
11
|
+
the name keeps resolving throughout.
|
|
12
|
+
|
|
13
|
+
Each build is created carrying the marker alias ``<alias>.unswapped``, and
|
|
14
|
+
the swap removes the marker in the same ``_aliases`` call that adds the
|
|
15
|
+
alias. A build that still carries the marker never went live: its process
|
|
16
|
+
was killed before cleanup could run, or it was kept with ``keep_failed``.
|
|
17
|
+
Pruning deletes such builds and keeps the newest rollback copies.
|
|
18
|
+
|
|
19
|
+
Every index named ``<alias>-*`` that carries no alias is treated as a
|
|
20
|
+
rollback copy of that alias and may be pruned, so that prefix must not be
|
|
21
|
+
used for anything else.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import time
|
|
26
|
+
from typing import Callable
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Rollback copies kept per alias, besides the live index.
|
|
31
|
+
KEEP_PREVIOUS = 2
|
|
32
|
+
|
|
33
|
+
HEALTH_POLL_SECONDS = 1.0
|
|
34
|
+
HEALTH_WAIT_SECONDS = 300.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConcreteIndexError(RuntimeError):
|
|
38
|
+
"""A concrete index sits under the alias name and no migration was asked
|
|
39
|
+
for."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dated_index_name(alias: str, suffix: str) -> str:
|
|
43
|
+
return f"{alias}-{suffix}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def unswapped_alias(alias: str) -> str:
|
|
47
|
+
"""The marker alias carried by builds of ``alias`` that were never live."""
|
|
48
|
+
return f"{alias}.unswapped"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def concrete_index_exists(es_client, alias: str) -> bool:
|
|
52
|
+
"""Return True if ``alias`` names a concrete index rather than an alias."""
|
|
53
|
+
return not es_client.indices.exists_alias(name=alias) and es_client.indices.exists(
|
|
54
|
+
index=alias
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def aliased_indexes(es_client, alias: str) -> list[str]:
|
|
59
|
+
"""Return the indexes ``alias`` points to, or an empty list if there is no
|
|
60
|
+
such alias."""
|
|
61
|
+
if not es_client.indices.exists_alias(name=alias):
|
|
62
|
+
return []
|
|
63
|
+
return sorted(es_client.indices.get_alias(name=alias))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def wait_for_index(
|
|
67
|
+
es_client,
|
|
68
|
+
index: str,
|
|
69
|
+
wait_seconds: float = HEALTH_WAIT_SECONDS,
|
|
70
|
+
poll_seconds: float = HEALTH_POLL_SECONDS,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Poll until ``index`` is at least yellow.
|
|
73
|
+
|
|
74
|
+
opensearch-py's ``cluster.health(..., timeout=...)`` clashes with the
|
|
75
|
+
client's own request timeout, so this polls instead of asking the server
|
|
76
|
+
to wait.
|
|
77
|
+
"""
|
|
78
|
+
deadline = time.monotonic() + wait_seconds
|
|
79
|
+
while True:
|
|
80
|
+
status = es_client.cluster.health(index=index)["status"]
|
|
81
|
+
if status in ("green", "yellow"):
|
|
82
|
+
return
|
|
83
|
+
if time.monotonic() >= deadline:
|
|
84
|
+
raise TimeoutError(f"Index {index!r} still {status} after {wait_seconds}s")
|
|
85
|
+
time.sleep(poll_seconds)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_and_swap(
|
|
89
|
+
es_client,
|
|
90
|
+
alias: str,
|
|
91
|
+
index_body: dict,
|
|
92
|
+
suffix: str,
|
|
93
|
+
load: Callable[[str], None],
|
|
94
|
+
validate: Callable[[str, str | None], None],
|
|
95
|
+
*,
|
|
96
|
+
migrate_suffix: str | None = None,
|
|
97
|
+
keep_failed: bool = False,
|
|
98
|
+
keep_previous: int = KEEP_PREVIOUS,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Build ``<alias>-<suffix>``, validate it and point ``alias`` at it.
|
|
101
|
+
|
|
102
|
+
Two builds of one alias must not run at once. The first to swap prunes
|
|
103
|
+
the other's index, which still carries the marker alias.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
es_client: OpenSearch client.
|
|
107
|
+
alias: The name readers and writers use.
|
|
108
|
+
index_body: Settings and mappings for the new index.
|
|
109
|
+
suffix: Version or date that makes the new index name unique.
|
|
110
|
+
load: Called with the new index name to fill it.
|
|
111
|
+
validate: Called with the new index name and the live index name
|
|
112
|
+
(``None`` if nothing is live yet). Raise to stop the swap.
|
|
113
|
+
migrate_suffix: Needed only when a concrete index sits under
|
|
114
|
+
``alias``. The concrete index is cloned to
|
|
115
|
+
``<alias>-<migrate_suffix>`` as the rollback copy, so this should
|
|
116
|
+
be the version that index holds.
|
|
117
|
+
keep_failed: Keep the new index if loading or validation fails,
|
|
118
|
+
for inspection. By default it is deleted. A kept index still
|
|
119
|
+
carries the marker, so the next successful build prunes it.
|
|
120
|
+
keep_previous: Rollback copies to keep after the swap.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The name of the new index, which ``alias`` now points to.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ConcreteIndexError: A concrete index sits under ``alias`` and
|
|
127
|
+
``migrate_suffix`` is ``None``.
|
|
128
|
+
ValueError: The new index name or the rollback copy's name is
|
|
129
|
+
already taken.
|
|
130
|
+
"""
|
|
131
|
+
new_index = dated_index_name(alias, suffix)
|
|
132
|
+
marker = unswapped_alias(alias)
|
|
133
|
+
if es_client.indices.exists(index=new_index):
|
|
134
|
+
raise ValueError(f"Index {new_index!r} already exists")
|
|
135
|
+
|
|
136
|
+
migrating = concrete_index_exists(es_client, alias)
|
|
137
|
+
if migrating:
|
|
138
|
+
if migrate_suffix is None:
|
|
139
|
+
raise ConcreteIndexError(
|
|
140
|
+
f"{alias!r} is a concrete index; pass migrate_suffix to migrate it"
|
|
141
|
+
)
|
|
142
|
+
clone = dated_index_name(alias, migrate_suffix)
|
|
143
|
+
if clone == new_index:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Rollback copy and new index would both be named {new_index!r}"
|
|
146
|
+
)
|
|
147
|
+
if es_client.indices.exists(index=clone):
|
|
148
|
+
raise ValueError(f"Index {clone!r} already exists")
|
|
149
|
+
live_indexes = []
|
|
150
|
+
live = alias
|
|
151
|
+
else:
|
|
152
|
+
live_indexes = aliased_indexes(es_client, alias)
|
|
153
|
+
if len(live_indexes) > 1:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Alias {alias!r} points to several indexes: {live_indexes}"
|
|
156
|
+
)
|
|
157
|
+
live = live_indexes[0] if live_indexes else None
|
|
158
|
+
|
|
159
|
+
if migrating:
|
|
160
|
+
_clone_for_rollback(es_client, alias, clone)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
logger.info(f"Creating index {new_index!r}")
|
|
164
|
+
body = {
|
|
165
|
+
**index_body,
|
|
166
|
+
"aliases": {**index_body.get("aliases", {}), marker: {}},
|
|
167
|
+
}
|
|
168
|
+
es_client.indices.create(index=new_index, body=body)
|
|
169
|
+
wait_for_index(es_client, new_index)
|
|
170
|
+
load(new_index)
|
|
171
|
+
es_client.indices.refresh(index=new_index)
|
|
172
|
+
validate(new_index, live)
|
|
173
|
+
except Exception:
|
|
174
|
+
logger.exception(f"Build of {new_index!r} failed; {alias!r} is unchanged")
|
|
175
|
+
if not keep_failed and es_client.indices.exists(index=new_index):
|
|
176
|
+
es_client.indices.delete(index=new_index)
|
|
177
|
+
if migrating:
|
|
178
|
+
# The concrete index stays live, so a later run makes a fresh copy.
|
|
179
|
+
es_client.indices.delete(index=clone)
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
if migrating:
|
|
183
|
+
actions = [{"remove_index": {"index": alias}}]
|
|
184
|
+
else:
|
|
185
|
+
actions = [{"remove": {"index": i, "alias": alias}} for i in live_indexes]
|
|
186
|
+
actions.append({"remove": {"index": new_index, "alias": marker}})
|
|
187
|
+
actions.append({"add": {"index": new_index, "alias": alias}})
|
|
188
|
+
es_client.indices.update_aliases(body={"actions": actions})
|
|
189
|
+
logger.info(f"Alias {alias!r} now points to {new_index!r} (was {live!r})")
|
|
190
|
+
|
|
191
|
+
prune(es_client, alias, keep_previous)
|
|
192
|
+
return new_index
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _clone_for_rollback(es_client, index: str, clone: str) -> None:
|
|
196
|
+
"""Copy the concrete ``index`` to ``clone``.
|
|
197
|
+
|
|
198
|
+
``_clone`` needs a write-blocked source. The block is lifted from the
|
|
199
|
+
source straight after, so it lasts only as long as the copy. The clone
|
|
200
|
+
keeps it; ``rollback`` clears it.
|
|
201
|
+
"""
|
|
202
|
+
logger.info(f"Cloning concrete index {index!r} to {clone!r}")
|
|
203
|
+
es_client.indices.put_settings(index=index, body={"index.blocks.write": True})
|
|
204
|
+
try:
|
|
205
|
+
es_client.indices.clone(index=index, target=clone)
|
|
206
|
+
wait_for_index(es_client, clone)
|
|
207
|
+
finally:
|
|
208
|
+
es_client.indices.put_settings(index=index, body={"index.blocks.write": None})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def prune(es_client, alias: str, keep_previous: int = KEEP_PREVIOUS) -> list[str]:
|
|
212
|
+
"""Delete unswapped builds of ``alias``, and the rollback copies beyond
|
|
213
|
+
the newest ``keep_previous``.
|
|
214
|
+
|
|
215
|
+
An unswapped build carries ``unswapped_alias(alias)``. Indexes that carry
|
|
216
|
+
any other alias are never deleted. Age is the index's creation date,
|
|
217
|
+
since suffixes such as ROR versions don't sort as strings.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The deleted index names.
|
|
221
|
+
"""
|
|
222
|
+
found = es_client.indices.get_alias(index=f"{alias}-*")
|
|
223
|
+
marker = unswapped_alias(alias)
|
|
224
|
+
# Unlike rollback, spare a marked index that also carries another alias.
|
|
225
|
+
orphans = [
|
|
226
|
+
name for name, info in found.items() if set(info.get("aliases", {})) == {marker}
|
|
227
|
+
]
|
|
228
|
+
for name in orphans:
|
|
229
|
+
logger.info(f"Pruning orphan {name!r}, a build that was never live")
|
|
230
|
+
es_client.indices.delete(index=name)
|
|
231
|
+
candidates = [name for name, info in found.items() if not info.get("aliases")]
|
|
232
|
+
if len(candidates) <= keep_previous:
|
|
233
|
+
return orphans
|
|
234
|
+
settings = es_client.indices.get_settings(
|
|
235
|
+
index=",".join(candidates), name="index.creation_date"
|
|
236
|
+
)
|
|
237
|
+
candidates.sort(
|
|
238
|
+
key=lambda name: int(settings[name]["settings"]["index"]["creation_date"]),
|
|
239
|
+
reverse=True,
|
|
240
|
+
)
|
|
241
|
+
to_delete = candidates[keep_previous:]
|
|
242
|
+
for name in to_delete:
|
|
243
|
+
logger.info(f"Pruning rollback copy {name!r}")
|
|
244
|
+
es_client.indices.delete(index=name)
|
|
245
|
+
return orphans + to_delete
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def rollback(es_client, alias: str, index: str, *, force: bool = False) -> None:
|
|
249
|
+
"""Point ``alias`` back at an earlier ``index``.
|
|
250
|
+
|
|
251
|
+
Clears ``index.blocks.write`` on ``index`` first: a rollback copy made
|
|
252
|
+
during migration inherits the block, and writes through the alias would
|
|
253
|
+
fail with it in place.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
ValueError: ``index`` is an unswapped build (it still carries
|
|
257
|
+
``unswapped_alias(alias)``) and ``force`` is False. Such a build
|
|
258
|
+
was never live, and may not have been fully loaded or validated.
|
|
259
|
+
"""
|
|
260
|
+
if concrete_index_exists(es_client, alias):
|
|
261
|
+
raise ConcreteIndexError(f"{alias!r} is a concrete index, not an alias")
|
|
262
|
+
marker = unswapped_alias(alias)
|
|
263
|
+
unswapped = index in aliased_indexes(es_client, marker)
|
|
264
|
+
if unswapped and not force:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"{index!r} was never live under {alias!r}; pass force=True to use it"
|
|
267
|
+
)
|
|
268
|
+
es_client.indices.put_settings(index=index, body={"index.blocks.write": None})
|
|
269
|
+
actions = [
|
|
270
|
+
{"remove": {"index": i, "alias": alias}}
|
|
271
|
+
for i in aliased_indexes(es_client, alias)
|
|
272
|
+
if i != index
|
|
273
|
+
]
|
|
274
|
+
if unswapped:
|
|
275
|
+
actions.append({"remove": {"index": index, "alias": marker}})
|
|
276
|
+
actions.append({"add": {"index": index, "alias": alias}})
|
|
277
|
+
es_client.indices.update_aliases(body={"actions": actions})
|
|
278
|
+
logger.info(f"Alias {alias!r} rolled back to {index!r}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Create and initialize the Crossref grants OpenSearch index.
|
|
2
|
+
|
|
3
|
+
The index holds one document per grant DOI. Award numbers and funder
|
|
4
|
+
identifiers are stored as ``keyword`` fields (normalized at index time) so that
|
|
5
|
+
matching can be done with cheap, deterministic ``term``/``terms`` queries
|
|
6
|
+
rather than free-text relevance search. See
|
|
7
|
+
``crossref_matcher/indexes/grants/normalize.py`` for the
|
|
8
|
+
normalization rules.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from opensearchpy import OpenSearch
|
|
12
|
+
from opensearchpy.exceptions import NotFoundError
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
INDEX = "grant"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def index_body() -> dict:
|
|
22
|
+
"""Return the OpenSearch settings and mappings for the grants index."""
|
|
23
|
+
return {
|
|
24
|
+
"settings": {
|
|
25
|
+
"number_of_shards": 1,
|
|
26
|
+
},
|
|
27
|
+
"mappings": {
|
|
28
|
+
"_meta": {
|
|
29
|
+
# Identifies the dataset version indexed, e.g. a snapshot date.
|
|
30
|
+
# Filled in when indexing.
|
|
31
|
+
"target_data": "",
|
|
32
|
+
},
|
|
33
|
+
"properties": {
|
|
34
|
+
# Original grant DOI (as deposited); used to build the output id.
|
|
35
|
+
"doi": {"type": "keyword"},
|
|
36
|
+
# Normalized (lowercased, resolver-stripped) DOI, for the direct
|
|
37
|
+
# grant-DOI fast path where the incoming award *is* a grant DOI.
|
|
38
|
+
"doi_norm": {"type": "keyword"},
|
|
39
|
+
# Normalized grant award number(s). Queried with the variants of
|
|
40
|
+
# the incoming award string.
|
|
41
|
+
"award": {"type": "keyword"},
|
|
42
|
+
# Normalized Funder Registry (Fundref) DOIs for this grant.
|
|
43
|
+
"funder_ids": {"type": "keyword"},
|
|
44
|
+
# Normalized ROR IDs for this grant.
|
|
45
|
+
"ror_ids": {"type": "keyword"},
|
|
46
|
+
# ROR IDs hierarchically related to this grant's funders: the
|
|
47
|
+
# transitive ancestors and descendants (ROR parent/child
|
|
48
|
+
# relationships) of every ID in ror_ids, excluding ror_ids
|
|
49
|
+
# itself so the exact and hierarchy confidence tiers stay
|
|
50
|
+
# disjoint. Funders with no ROR record of their own (e.g. EU
|
|
51
|
+
# funding programmes like H2020) are canonicalized to their
|
|
52
|
+
# nearest Fundref ancestor's ROR before ror_ids is built (see
|
|
53
|
+
# the packaged funder->ROR mapping), so their registry
|
|
54
|
+
# hierarchy is captured here rather than in a separate field.
|
|
55
|
+
"ror_family": {"type": "keyword"},
|
|
56
|
+
# Publisher (member) name, carried through for reporting/debugging.
|
|
57
|
+
"publisher": {"type": "keyword"},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def initialize_index(index: str, es_client: OpenSearch, body: dict) -> dict:
|
|
64
|
+
"""Create the OpenSearch index and return the client response.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
index: Name of the index to create.
|
|
68
|
+
es_client: OpenSearch client instance.
|
|
69
|
+
body: Index settings and mappings payload.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
OpenSearch response payload from index creation.
|
|
73
|
+
"""
|
|
74
|
+
response = es_client.indices.create(index=index, body=body)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
logging.basicConfig(level=logging.INFO)
|
|
80
|
+
logging.getLogger("opensearch").setLevel(logging.WARNING)
|
|
81
|
+
import argparse
|
|
82
|
+
|
|
83
|
+
parser = argparse.ArgumentParser(
|
|
84
|
+
description="Create OpenSearch index for grant matching"
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--force",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Force delete and recreate the index if it already exists",
|
|
90
|
+
)
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
|
|
93
|
+
from crossref_matcher.matching.search import ES_CLIENT as es_client
|
|
94
|
+
|
|
95
|
+
# Check if index already exists
|
|
96
|
+
index_exists = es_client.indices.exists(index=INDEX)
|
|
97
|
+
|
|
98
|
+
if index_exists and not args.force:
|
|
99
|
+
logger.info(
|
|
100
|
+
f"Index '{INDEX}' already exists. Use --force to delete and recreate it."
|
|
101
|
+
)
|
|
102
|
+
exit(0)
|
|
103
|
+
|
|
104
|
+
# Delete index if it exists and force flag is set
|
|
105
|
+
if index_exists and args.force:
|
|
106
|
+
try:
|
|
107
|
+
response = es_client.indices.delete(index=INDEX)
|
|
108
|
+
logger.info(f"Deleted existing index: {response}")
|
|
109
|
+
except NotFoundError:
|
|
110
|
+
logger.warning("Index not found during deletion")
|
|
111
|
+
|
|
112
|
+
index_body_content = index_body()
|
|
113
|
+
|
|
114
|
+
response = initialize_index(INDEX, es_client, index_body_content)
|
|
115
|
+
logger.info(f"Created index '{INDEX}': {response}")
|