]> git.openstreetmap.org Git - nominatim.git/blob - docs/develop/ICU-Tokenizer-Modules.md
2427ab11629dc589c3652acc124cd6dccd0b5e3e
[nominatim.git] / docs / develop / ICU-Tokenizer-Modules.md
1 # Writing custom sanitizer and token analysis modules for the ICU tokenizer
2
3 The [ICU tokenizer](../customize/Tokenizers.md#icu-tokenizer) provides a
4 highly customizable method to pre-process and normalize the name information
5 of the input data before it is added to the search index. It comes with a
6 selection of sanitizers and token analyzers which you can use to adapt your
7 installation to your needs. If the provided modules are not enough, you can
8 also provide your own implementations. This section describes the API
9 of sanitizers and token analysis.
10
11 !!! warning
12     This API is currently in early alpha status. While this API is meant to
13     be a public API on which other sanitizers and token analyzers may be
14     implemented, it is not guaranteed to be stable at the moment.
15
16
17 ## Using non-standard sanitizers and token analyzers
18
19 Sanitizer names (in the `step` property) and token analysis names (in the
20 `analyzer`) may refer to externally supplied modules. There are two ways
21 to include external modules: through a library or from the project directory.
22
23 To include a module from a library, use the absolute import path as name and
24 make sure the library can be found in your PYTHONPATH.
25
26 To use a custom module without creating a library, you can put the module
27 somewhere in your project directory and then use the relative path to the
28 file. Include the whole name of the file including the `.py` ending.
29
30 ## Custom sanitizer modules
31
32 A sanitizer module must export a single factory function `create` with the
33 following signature:
34
35 ``` python
36 def create(config: SanitizerConfig) -> Callable[[ProcessInfo], None]
37 ```
38
39 The function receives the custom configuration for the sanitizer and must
40 return a callable (function or class) that transforms the name and address
41 terms of a place. When a place is processed, then a `ProcessInfo` object
42 is created from the information that was queried from the database. This
43 object is sequentially handed to each configured sanitizer, so that each
44 sanitizer receives the result of processing from the previous sanitizer.
45 After the last sanitizer is finished, the resulting name and address lists
46 are forwarded to the token analysis module.
47
48 Sanitizer functions are instantiated once and then called for each place
49 that is imported or updated. They don't need to be thread-safe.
50 If multi-threading is used, each thread creates their own instance of
51 the function.
52
53 ### Sanitizer configuration
54
55 ::: nominatim.tokenizer.sanitizers.config.SanitizerConfig
56     rendering:
57         show_source: no
58         heading_level: 6
59
60 ### The sanitation function
61
62 The sanitation function receives a single object of type `ProcessInfo`
63 which has with three members:
64
65  * `place`: read-only information about the place being processed.
66    See PlaceInfo below.
67  * `names`: The current list of names for the place. Each name is a
68    PlaceName object.
69  * `address`: The current list of address names for the place. Each name
70    is a PlaceName object.
71
72 While the `place` member is provided for information only, the `names` and
73 `address` lists are meant to be manipulated by the sanitizer. It may add and
74 remove entries, change information within a single entry (for example by
75 adding extra attributes) or completely replace the list with a different one.
76
77 ### Example: Filter for US street prefixes
78
79 The following sanitizer removes the directional prefixes from street names
80 in the US:
81
82 ``` python
83 import re
84
85 def _filter_function(obj):
86     if obj.place.country_code == 'us' \
87        and obj.place.rank_address >= 26 and obj.place.rank_address <= 27:
88         for name in obj.names:
89             name.name = re.sub(r'^(north|south|west|east) ',
90                                '',
91                                name.name,
92                                flags=re.IGNORECASE)
93
94 def create(config):
95     return _filter_function
96 ```
97
98 This is the most simple form of a sanitizer module. If defines a single
99 filter function and implements the required `create()` function by returning
100 the filter.
101
102 The filter function first checks if the object is interesting for the
103 sanitizer. Namely it checks if the place is in the US (through `country_code`)
104 and it the place is a street (a `rank_address` of 26 or 27). If the
105 conditions are met, then it goes through all available names and replaces
106 any removes any leading direction prefix using a simple regular expression.
107
108 Save the source code in a file in your project directory, for example as
109 `us_streets.py`. Then you can use the sanitizer in your `icu_tokenizer.yaml`:
110
111 ```
112 ...
113 sanitizers:
114     - step: us_streets.py
115 ...
116 ```
117
118 For more sanitizer examples, have a look at the sanitizers provided by Nominatim.
119 They can be found in the directory `nominatim/tokenizer/sanitizers`.
120
121 !!! warning
122     This example is just a simplified show case on how to create a sanitizer.
123     It is not really read for real-world use: while the sanitizer would
124     correcly transform `West 5th Street` into `5th Street`. it would also
125     shorten a simple `North Street` to `Street`.
126
127 #### PlaceInfo - information about the place
128
129 ::: nominatim.data.place_info.PlaceInfo
130     rendering:
131         show_source: no
132         heading_level: 6
133
134
135 #### PlaceName - extended naming information
136
137 ::: nominatim.data.place_name.PlaceName
138     rendering:
139         show_source: no
140         heading_level: 6
141
142 ## Custom token analysis module
143
144 Setup of a token analyser is split into two parts: configuration and
145 analyser factory. A token analysis module must therefore implement two
146 functions:
147
148 ::: nominatim.tokenizer.token_analysis.base.AnalysisModule
149     rendering:
150         show_source: no
151         heading_level: 6
152
153
154 ::: nominatim.tokenizer.token_analysis.base.Analyzer
155     rendering:
156         show_source: no
157         heading_level: 6
158
159 ### Example: Creating acronym variants for long names
160
161 The following example of a token analysis module creates acronyms from
162 very long names and adds them as a variant:
163
164 ``` python
165 class AcronymMaker:
166     """ This class is the actual analyzer.
167     """
168     def __init__(self, norm, trans):
169         self.norm = norm
170         self.trans = trans
171
172
173     def get_canonical_id(self, name):
174         # In simple cases, the normalized name can be used as a canonical id.
175         return self.norm.transliterate(name.name).strip()
176
177
178     def compute_variants(self, name):
179         # The transliterated form of the name always makes up a variant.
180         variants = [self.trans.transliterate(name)]
181
182         # Only create acronyms from very long words.
183         if len(name) > 20:
184             # Take the first letter from each word to form the acronym.
185             acronym = ''.join(w[0] for w in name.split())
186             # If that leds to an acronym with at least three letters,
187             # add the resulting acronym as a variant.
188             if len(acronym) > 2:
189                 # Never forget to transliterate the variants before returning them.
190                 variants.append(self.trans.transliterate(acronym))
191
192         return variants
193
194 # The following two functions are the module interface.
195
196 def configure(rules, normalizer, transliterator):
197     # There is no configuration to parse and no data to set up.
198     # Just return an empty configuration.
199     return None
200
201
202 def create(normalizer, transliterator, config):
203     # Return a new instance of our token analysis class above.
204     return AcronymMaker(normalizer, transliterator)
205 ```
206
207 Given the name `Trans-Siberian Railway`, the code above would return the full
208 name `Trans-Siberian Railway` and the acronym `TSR` as variant, so that
209 searching would work for both.
210
211 ## Sanitizers vs. Token analysis - what to use for variants?
212
213 It is not always clear when to implement variations in the sanitizer and
214 when to write a token analysis module. Just take the acronym example
215 above: it would also have been possible to write a sanitizer which adds the
216 acronym as an additional name to the name list. The result would have been
217 similar. So which should be used when?
218
219 The most important thing to keep in mind is that variants created by the
220 token analysis are only saved in the word lookup table. They do not need
221 extra space in the search index. If there are many spelling variations, this
222 can mean quite a significant amount of space is saved.
223
224 When creating additional names with a sanitizer, these names are completely
225 independent. In particular, they can be fed into different token analysis
226 modules. This gives a much greater flexibility but at the price that the
227 additional names increase the size of the search index.
228