]> git.openstreetmap.org Git - nominatim.git/blob - docs/develop/ICU-Tokenizer-Modules.md
Merge pull request #3658 from lonvia/minor-query-parsing-optimisations
[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 modules
18
19 Sanitizer names (in the `step` property), token analysis names (in the
20 `analyzer`) and query preprocessor names (in the `step` property)
21 may refer to externally supplied modules. There are two ways
22 to include external modules: through a library or from the project directory.
23
24 To include a module from a library, use the absolute import path as name and
25 make sure the library can be found in your PYTHONPATH.
26
27 To use a custom module without creating a library, you can put the module
28 somewhere in your project directory and then use the relative path to the
29 file. Include the whole name of the file including the `.py` ending.
30
31 ## Custom query preprocessors
32
33 A query preprocessor must export a single factory function `create` with
34 the following signature:
35
36 ``` python
37 create(self, config: QueryConfig) -> Callable[[list[Phrase]], list[Phrase]]
38 ```
39
40 The function receives the custom configuration for the preprocessor and
41 returns a callable (function or class) with the actual preprocessing
42 code. When a query comes in, then the callable gets a list of phrases
43 and needs to return the transformed list of phrases. The list and phrases
44 may be changed in place or a completely new list may be generated.
45
46 The `QueryConfig` is a simple dictionary which contains all configuration
47 options given in the yaml configuration of the ICU tokenizer. It is up to
48 the function to interpret the values.
49
50 A `nominatim_api.search.Phrase` describes a part of the query that contains one or more independent
51 search terms. Breaking a query into phrases helps reducing the number of
52 possible tokens Nominatim has to take into account. However a phrase break
53 is definitive: a multi-term search word cannot go over a phrase break.
54 A Phrase object has two fields:
55
56  * `ptype` further refines the type of phrase (see list below)
57  * `text` contains the query text for the phrase
58
59 The order of phrases matters to Nominatim when doing further processing.
60 Thus, while you may split or join phrases, you should not reorder them
61 unless you really know what you are doing.
62
63 Phrase types can further help narrowing down how the tokens in the phrase
64 are interpreted. The following phrase types are known:
65
66 | Name           | Description |
67 |----------------|-------------|
68 | PHRASE_ANY     | No specific designation (i.e. source is free-form query) |
69 | PHRASE_AMENITY | Contains name or type of a POI |
70 | PHRASE_STREET  | Contains a street name optionally with a housenumber |
71 | PHRASE_CITY    | Contains the postal city |
72 | PHRASE_COUNTY  | Contains the equivalent of a county |
73 | PHRASE_STATE   | Contains a state or province |
74 | PHRASE_POSTCODE| Contains a postal code |
75 | PHRASE_COUNTRY | Contains the country name or code |
76
77
78 ## Custom sanitizer modules
79
80 A sanitizer module must export a single factory function `create` with the
81 following signature:
82
83 ``` python
84 def create(config: SanitizerConfig) -> Callable[[ProcessInfo], None]
85 ```
86
87 The function receives the custom configuration for the sanitizer and must
88 return a callable (function or class) that transforms the name and address
89 terms of a place. When a place is processed, then a `ProcessInfo` object
90 is created from the information that was queried from the database. This
91 object is sequentially handed to each configured sanitizer, so that each
92 sanitizer receives the result of processing from the previous sanitizer.
93 After the last sanitizer is finished, the resulting name and address lists
94 are forwarded to the token analysis module.
95
96 Sanitizer functions are instantiated once and then called for each place
97 that is imported or updated. They don't need to be thread-safe.
98 If multi-threading is used, each thread creates their own instance of
99 the function.
100
101 ### Sanitizer configuration
102
103 ::: nominatim_db.tokenizer.sanitizers.config.SanitizerConfig
104     options:
105         heading_level: 6
106
107 ### The main filter function of the sanitizer
108
109 The filter function receives a single object of type `ProcessInfo`
110 which has with three members:
111
112  * `place: PlaceInfo`: read-only information about the place being processed.
113    See PlaceInfo below.
114  * `names: List[PlaceName]`: The current list of names for the place.
115  * `address: List[PlaceName]`: The current list of address names for the place.
116
117 While the `place` member is provided for information only, the `names` and
118 `address` lists are meant to be manipulated by the sanitizer. It may add and
119 remove entries, change information within a single entry (for example by
120 adding extra attributes) or completely replace the list with a different one.
121
122 #### PlaceInfo - information about the place
123
124 ::: nominatim_db.data.place_info.PlaceInfo
125     options:
126         heading_level: 6
127
128
129 #### PlaceName - extended naming information
130
131 ::: nominatim_db.data.place_name.PlaceName
132     options:
133         heading_level: 6
134
135
136 ### Example: Filter for US street prefixes
137
138 The following sanitizer removes the directional prefixes from street names
139 in the US:
140
141 !!! example
142     ``` python
143     import re
144
145     def _filter_function(obj):
146         if obj.place.country_code == 'us' \
147            and obj.place.rank_address >= 26 and obj.place.rank_address <= 27:
148             for name in obj.names:
149                 name.name = re.sub(r'^(north|south|west|east) ',
150                                    '',
151                                    name.name,
152                                    flags=re.IGNORECASE)
153
154     def create(config):
155         return _filter_function
156     ```
157
158 This is the most simple form of a sanitizer module. If defines a single
159 filter function and implements the required `create()` function by returning
160 the filter.
161
162 The filter function first checks if the object is interesting for the
163 sanitizer. Namely it checks if the place is in the US (through `country_code`)
164 and it the place is a street (a `rank_address` of 26 or 27). If the
165 conditions are met, then it goes through all available names and
166 removes any leading directional prefix using a simple regular expression.
167
168 Save the source code in a file in your project directory, for example as
169 `us_streets.py`. Then you can use the sanitizer in your `icu_tokenizer.yaml`:
170
171 ``` yaml
172 ...
173 sanitizers:
174     - step: us_streets.py
175 ...
176 ```
177
178 !!! warning
179     This example is just a simplified show case on how to create a sanitizer.
180     It is not really meant for real-world use: while the sanitizer would
181     correctly transform `West 5th Street` into `5th Street`. it would also
182     shorten a simple `North Street` to `Street`.
183
184 For more sanitizer examples, have a look at the sanitizers provided by Nominatim.
185 They can be found in the directory
186 [`src/nominatim_db/tokenizer/sanitizers`](https://github.com/osm-search/Nominatim/tree/master/src/nominatim_db/tokenizer/sanitizers).
187
188
189 ## Custom token analysis module
190
191 ::: nominatim_db.tokenizer.token_analysis.base.AnalysisModule
192     options:
193         heading_level: 6
194
195
196 ::: nominatim_db.tokenizer.token_analysis.base.Analyzer
197     options:
198         heading_level: 6
199
200 ### Example: Creating acronym variants for long names
201
202 The following example of a token analysis module creates acronyms from
203 very long names and adds them as a variant:
204
205 ``` python
206 class AcronymMaker:
207     """ This class is the actual analyzer.
208     """
209     def __init__(self, norm, trans):
210         self.norm = norm
211         self.trans = trans
212
213
214     def get_canonical_id(self, name):
215         # In simple cases, the normalized name can be used as a canonical id.
216         return self.norm.transliterate(name.name).strip()
217
218
219     def compute_variants(self, name):
220         # The transliterated form of the name always makes up a variant.
221         variants = [self.trans.transliterate(name)]
222
223         # Only create acronyms from very long words.
224         if len(name) > 20:
225             # Take the first letter from each word to form the acronym.
226             acronym = ''.join(w[0] for w in name.split())
227             # If that leds to an acronym with at least three letters,
228             # add the resulting acronym as a variant.
229             if len(acronym) > 2:
230                 # Never forget to transliterate the variants before returning them.
231                 variants.append(self.trans.transliterate(acronym))
232
233         return variants
234
235 # The following two functions are the module interface.
236
237 def configure(rules, normalizer, transliterator):
238     # There is no configuration to parse and no data to set up.
239     # Just return an empty configuration.
240     return None
241
242
243 def create(normalizer, transliterator, config):
244     # Return a new instance of our token analysis class above.
245     return AcronymMaker(normalizer, transliterator)
246 ```
247
248 Given the name `Trans-Siberian Railway`, the code above would return the full
249 name `Trans-Siberian Railway` and the acronym `TSR` as variant, so that
250 searching would work for both.
251
252 ## Sanitizers vs. Token analysis - what to use for variants?
253
254 It is not always clear when to implement variations in the sanitizer and
255 when to write a token analysis module. Just take the acronym example
256 above: it would also have been possible to write a sanitizer which adds the
257 acronym as an additional name to the name list. The result would have been
258 similar. So which should be used when?
259
260 The most important thing to keep in mind is that variants created by the
261 token analysis are only saved in the word lookup table. They do not need
262 extra space in the search index. If there are many spelling variations, this
263 can mean quite a significant amount of space is saved.
264
265 When creating additional names with a sanitizer, these names are completely
266 independent. In particular, they can be fed into different token analysis
267 modules. This gives a much greater flexibility but at the price that the
268 additional names increase the size of the search index.
269