Add a prebuilt ICU Analyzer (#34958)

The ICU plugin provides the building blocks of an analysis chain, but doesn't actually have a prebuilt analyzer. It would be a better for users if there was a simple analyzer that they could use out of the box, and also something we can point to from the CJK Analyzer docs as a superior alternative.

Relates to #34285
This commit is contained in:
Alan Woodward 2018-11-21 09:00:48 +00:00 committed by GitHub
parent e8ec4fad7b
commit f6a43b5939
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 192 additions and 0 deletions

View File

@ -26,6 +26,24 @@ characters.
:plugin_name: analysis-icu
include::install_remove.asciidoc[]
[[analysis-icu-analyzer]]
==== ICU Analyzer
Performs basic normalization, tokenization and character folding, using the
`icu_normalizer` char filter, `icu_tokenizer` and `icu_normalizer` token filter
The following parameters are accepted:
[horizontal]
`method`::
Normalization method. Accepts `nfkc`, `nfc` or `nfkc_cf` (default)
`mode`::
Normalization mode. Accepts `compose` (default) or `decompose`.
[[analysis-icu-normalization-charfilter]]
==== ICU Normalization Character Filter

View File

@ -421,6 +421,9 @@ PUT /catalan_example
[[cjk-analyzer]]
===== `cjk` analyzer
NOTE: You may find that `icu_analyzer` in the ICU analysis plugin works better
for CJK text than the `cjk` analyzer. Experiment with your text and queries.
The `cjk` analyzer could be reimplemented as a `custom` analyzer as follows:
[source,js]

View File

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.index.analysis;
import com.ibm.icu.text.Normalizer2;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.icu.ICUFoldingFilter;
import org.apache.lucene.analysis.icu.ICUNormalizer2CharFilter;
import org.apache.lucene.analysis.icu.segmentation.ICUTokenizer;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.index.IndexSettings;
import java.io.Reader;
public class IcuAnalyzerProvider extends AbstractIndexAnalyzerProvider<Analyzer> {
private final Normalizer2 normalizer;
public IcuAnalyzerProvider(IndexSettings indexSettings, Environment environment, String name, Settings settings) {
super(indexSettings, name, settings);
String method = settings.get("method", "nfkc_cf");
String mode = settings.get("mode", "compose");
if (!"compose".equals(mode) && !"decompose".equals(mode)) {
throw new IllegalArgumentException("Unknown mode [" + mode + "] in analyzer [" + name +
"], expected one of [compose, decompose]");
}
Normalizer2 normalizer = Normalizer2.getInstance(
null, method, "compose".equals(mode) ? Normalizer2.Mode.COMPOSE : Normalizer2.Mode.DECOMPOSE);
this.normalizer = IcuNormalizerTokenFilterFactory.wrapWithUnicodeSetFilter(indexSettings, normalizer, settings);
}
@Override
public Analyzer get() {
return new Analyzer() {
@Override
protected Reader initReader(String fieldName, Reader reader) {
return new ICUNormalizer2CharFilter(reader, normalizer);
}
@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer source = new ICUTokenizer();
return new TokenStreamComponents(source, new ICUFoldingFilter(source));
}
};
}
}

View File

@ -21,8 +21,11 @@ package org.elasticsearch.plugin.analysis.icu;
import static java.util.Collections.singletonMap;
import org.apache.lucene.analysis.Analyzer;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.index.analysis.AnalyzerProvider;
import org.elasticsearch.index.analysis.CharFilterFactory;
import org.elasticsearch.index.analysis.IcuAnalyzerProvider;
import org.elasticsearch.index.analysis.IcuCollationTokenFilterFactory;
import org.elasticsearch.index.analysis.IcuFoldingTokenFilterFactory;
import org.elasticsearch.index.analysis.IcuNormalizerCharFilterFactory;
@ -60,6 +63,11 @@ public class AnalysisICUPlugin extends Plugin implements AnalysisPlugin, MapperP
return extra;
}
@Override
public Map<String, AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> getAnalyzers() {
return singletonMap("icu_analyzer", IcuAnalyzerProvider::new);
}
@Override
public Map<String, AnalysisProvider<TokenizerFactory>> getTokenizers() {
return singletonMap("icu_tokenizer", IcuTokenizerFactory::new);

View File

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.index.analysis;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.BaseTokenStreamTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.plugin.analysis.icu.AnalysisICUPlugin;
import org.elasticsearch.test.IndexSettingsModule;
import java.io.IOException;
import static org.hamcrest.Matchers.containsString;
public class IcuAnalyzerTests extends BaseTokenStreamTestCase {
public void testMixedAlphabetTokenization() throws IOException {
Settings settings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.build();
IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings);
String input = "안녕은하철도999극장판2.1981년8월8일.일본개봉작1999년재더빙video판";
AnalysisICUPlugin plugin = new AnalysisICUPlugin();
Analyzer analyzer = plugin.getAnalyzers().get("icu_analyzer").get(idxSettings, null, "icu", settings).get();
assertAnalyzesTo(analyzer, input,
new String[]{"안녕은하철도", "999", "극장판", "2.1981", "", "8", "", "8", "", "일본개봉작", "1999", "년재더빙", "video", ""});
}
public void testMiddleDots() throws IOException {
Settings settings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.build();
IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings);
String input = "경승지·산악·협곡·해협·곶·심연·폭포·호수·급류";
Analyzer analyzer = new IcuAnalyzerProvider(idxSettings, null, "icu", settings).get();
assertAnalyzesTo(analyzer, input,
new String[]{"경승지", "산악", "협곡", "해협", "", "심연", "폭포", "호수", "급류"});
}
public void testUnicodeNumericCharacters() throws IOException {
Settings settings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.build();
IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings);
String input = "① ② ③ ⑴ ⑵ ⑶ ¼ ⅓ ⅜ ¹ ² ³ ₁ ₂ ₃";
Analyzer analyzer = new IcuAnalyzerProvider(idxSettings, null, "icu", settings).get();
assertAnalyzesTo(analyzer, input,
new String[]{"1", "2", "3", "1", "2", "3", "1/4", "1/3", "3/8", "1", "2", "3", "1", "2", "3"});
}
public void testBadSettings() {
Settings settings = Settings.builder()
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.put("mode", "wrong")
.build();
IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
new IcuAnalyzerProvider(idxSettings, null, "icu", settings);
});
assertThat(e.getMessage(), containsString("Unknown mode [wrong] in analyzer [icu], expected one of [compose, decompose]"));
}
}