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.
Files changed (147) hide show
  1. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/MANIFEST.in +1 -0
  2. {crossref_matcher-1.3.2/crossref_matcher.egg-info → crossref_matcher-1.4.0}/PKG-INFO +1 -1
  3. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/README.md +27 -3
  4. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/MatchTask.py +1 -0
  5. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/app.py +17 -0
  6. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/all_links.py +26 -2
  7. crossref_matcher-1.4.0/crossref_matcher/indexes/aliased_index.py +278 -0
  8. crossref_matcher-1.4.0/crossref_matcher/indexes/grants/create_index.py +115 -0
  9. crossref_matcher-1.4.0/crossref_matcher/indexes/grants/index_data.py +445 -0
  10. crossref_matcher-1.4.0/crossref_matcher/indexes/grants/normalize.py +97 -0
  11. crossref_matcher-1.4.0/crossref_matcher/indexes/meta.py +21 -0
  12. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/create_index.py +57 -12
  13. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/index_data.py +192 -36
  14. crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations/update.py +137 -0
  15. crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations/validate.py +190 -0
  16. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/schemas.py +7 -0
  17. crossref_matcher-1.4.0/crossref_matcher/resources/VERSION.txt +1 -0
  18. crossref_matcher-1.4.0/crossref_matcher/resources/data/funders/ror_funder_mapping.csv.gz +0 -0
  19. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/plugins.json +2 -0
  20. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/__init__.py +11 -0
  21. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +9 -2
  22. crossref_matcher-1.4.0/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +1 -0
  23. crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/funder_ror.py +50 -0
  24. crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/strategy.py +309 -0
  25. crossref_matcher-1.4.0/crossref_matcher/strategies/grant/grant_award_matcher/version.txt +1 -0
  26. crossref_matcher-1.4.0/crossref_matcher/strategies/reference/parsed_isbn/__init__.py +0 -0
  27. crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv/__init__.py +0 -0
  28. crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv_orig/__init__.py +0 -0
  29. crossref_matcher-1.4.0/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
  30. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0/crossref_matcher.egg-info}/PKG-INFO +1 -1
  31. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/SOURCES.txt +15 -0
  32. crossref_matcher-1.3.2/crossref_matcher/resources/VERSION.txt +0 -1
  33. crossref_matcher-1.3.2/crossref_matcher/strategies/funder/funder_name_to_ror_search/version.txt +0 -1
  34. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/LICENSE +0 -0
  35. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/__init__.py +0 -0
  36. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/__init__.py +0 -0
  37. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/api_routes.py +0 -0
  38. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
  39. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/evaluation.py +0 -0
  40. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
  41. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/ror_utils.py +0 -0
  42. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/schemas.py +0 -0
  43. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/sparse_metrics_verification/__init__.py +0 -0
  44. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/evaluation/sparse_metrics_verification/verify_sparse_metrics.py +0 -0
  45. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/__init__.py +0 -0
  46. {crossref_matcher-1.3.2/crossref_matcher/indexes/preprint_matching → crossref_matcher-1.4.0/crossref_matcher/indexes/grants}/__init__.py +0 -0
  47. {crossref_matcher-1.3.2/crossref_matcher/indexes/ror_organizations → crossref_matcher-1.4.0/crossref_matcher/indexes/preprint_matching}/__init__.py +0 -0
  48. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
  49. {crossref_matcher-1.3.2/crossref_matcher/matching → crossref_matcher-1.4.0/crossref_matcher/indexes/ror_organizations}/__init__.py +0 -0
  50. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/opensearch_utils.py +0 -0
  51. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
  52. {crossref_matcher-1.3.2/crossref_matcher/resources → crossref_matcher-1.4.0/crossref_matcher/matching}/__init__.py +0 -0
  53. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/http_utils.py +0 -0
  54. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
  55. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/search.py +0 -0
  56. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/matching/utils.py +0 -0
  57. {crossref_matcher-1.3.2/crossref_matcher/resources/affiliation → crossref_matcher-1.4.0/crossref_matcher/resources}/__init__.py +0 -0
  58. {crossref_matcher-1.3.2/crossref_matcher/resources/data → crossref_matcher-1.4.0/crossref_matcher/resources/affiliation}/__init__.py +0 -0
  59. {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
  60. {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
  61. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
  62. {crossref_matcher-1.3.2/crossref_matcher/resources/data/datasets → crossref_matcher-1.4.0/crossref_matcher/resources/data/countries}/__init__.py +0 -0
  63. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
  64. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/countries.txt +0 -0
  65. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
  66. {crossref_matcher-1.3.2/crossref_matcher/resources/data/models → crossref_matcher-1.4.0/crossref_matcher/resources/data/datasets}/__init__.py +0 -0
  67. {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
  68. {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
  69. {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
  70. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
  71. {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
  72. {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
  73. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
  74. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
  75. {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
  76. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
  77. {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
  78. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
  79. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
  80. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
  81. {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
  82. {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
  83. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
  84. {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
  85. {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
  86. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
  87. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
  88. {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
  89. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/run_plugin_test.py +0 -0
  90. {crossref_matcher-1.3.2/crossref_matcher/strategies/affiliation/single_search → crossref_matcher-1.4.0/crossref_matcher/strategies/affiliation}/__init__.py +0 -0
  91. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
  92. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/matching.py +0 -0
  93. {crossref_matcher-1.3.2/crossref_matcher/strategies/funder → crossref_matcher-1.4.0/crossref_matcher/strategies/affiliation/multi_search}/__init__.py +0 -0
  94. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
  95. {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
  96. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
  97. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
  98. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/common.py +0 -0
  99. {crossref_matcher-1.3.2/crossref_matcher/strategies/healthcheck → crossref_matcher-1.4.0/crossref_matcher/strategies/funder}/__init__.py +0 -0
  100. {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
  101. {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint → crossref_matcher-1.4.0/crossref_matcher/strategies/grant}/__init__.py +0 -0
  102. {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
  103. {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.4.0/crossref_matcher/strategies/healthcheck}/__init__.py +0 -0
  104. {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
  105. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
  106. {crossref_matcher-1.3.2/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint}/__init__.py +0 -0
  107. {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
  108. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
  109. {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
  110. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
  111. {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
  112. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
  113. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
  114. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
  115. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
  116. {crossref_matcher-1.3.2/crossref_matcher/strategies/reference → crossref_matcher-1.4.0/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
  117. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
  118. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
  119. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
  120. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
  121. {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
  122. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
  123. {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
  124. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
  125. {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
  126. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
  127. {crossref_matcher-1.3.2/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.4.0/crossref_matcher/strategies/reference}/__init__.py +0 -0
  128. {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
  129. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
  130. {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
  131. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
  132. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
  133. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
  134. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
  135. {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
  136. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
  137. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
  138. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
  139. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
  140. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
  141. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher/strategies/versioning.py +0 -0
  142. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/dependency_links.txt +0 -0
  143. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/entry_points.txt +0 -0
  144. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/requires.txt +0 -0
  145. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/crossref_matcher.egg-info/top_level.txt +0 -0
  146. {crossref_matcher-1.3.2 → crossref_matcher-1.4.0}/setup.cfg +0 -0
  147. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossref-matcher
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: Crossref's matching API
5
5
  Home-page: https://gitlab.com/crossref/marple
6
6
  Author: Crossref
@@ -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 `s3://airflow.crossref.org` (research-only bucket), `X.Y.Z-dev.N` prerelease tags |
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
- `s3://airflow.crossref.org` to the reset state (the sync uses `--delete`, so
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
- unique_id = f"""{item["seq_no"]:06}_{this_row_true}"""
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}")