/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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.apache.lucene.analysis.icu.segmentation; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.lucene.analysis.util.ResourceLoader; import org.apache.lucene.analysis.util.ResourceLoaderAware; import org.apache.lucene.analysis.util.TokenizerFactory; import org.apache.lucene.util.AttributeFactory; import org.apache.lucene.util.IOUtils; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.lang.UProperty; import com.ibm.icu.lang.UScript; import com.ibm.icu.text.BreakIterator; import com.ibm.icu.text.RuleBasedBreakIterator; /** * Factory for {@link ICUTokenizer}. * Words are broken across script boundaries, then segmented according to * the BreakIterator and typing provided by the {@link DefaultICUTokenizerConfig}. * * <p> * To use the default set of per-script rules: * * <pre class="prettyprint" > * <fieldType name="text_icu" class="solr.TextField" positionIncrementGap="100"> * <analyzer> * <tokenizer class="solr.ICUTokenizerFactory"/> * </analyzer> * </fieldType></pre> * * <p> * You can customize this tokenizer's behavior by specifying per-script rule files, * which are compiled by the ICU RuleBasedBreakIterator. See the * <a href="http://userguide.icu-project.org/boundaryanalysis#TOC-RBBI-Rules" * >ICU RuleBasedBreakIterator syntax reference</a>. * * <p> * To add per-script rules, add a "rulefiles" argument, which should contain a * comma-separated list of <tt>code:rulefile</tt> pairs in the following format: * <a href="http://unicode.org/iso15924/iso15924-codes.html" * >four-letter ISO 15924 script code</a>, followed by a colon, then a resource * path. E.g. to specify rules for Latin (script code "Latn") and Cyrillic * (script code "Cyrl"): * * <pre class="prettyprint" > * <fieldType name="text_icu_custom" class="solr.TextField" positionIncrementGap="100"> * <analyzer> * <tokenizer class="solr.ICUTokenizerFactory" cjkAsWords="true" * rulefiles="Latn:my.Latin.rules.rbbi,Cyrl:my.Cyrillic.rules.rbbi"/> * </analyzer> * </fieldType></pre> */ public class ICUTokenizerFactory extends TokenizerFactory implements ResourceLoaderAware { static final String RULEFILES = "rulefiles"; private final Map<Integer,String> tailored; private ICUTokenizerConfig config; private final boolean cjkAsWords; private final boolean myanmarAsWords; /** Creates a new ICUTokenizerFactory */ public ICUTokenizerFactory(Map<String,String> args) { super(args); tailored = new HashMap<>(); String rulefilesArg = get(args, RULEFILES); if (rulefilesArg != null) { List<String> scriptAndResourcePaths = splitFileNames(rulefilesArg); for (String scriptAndResourcePath : scriptAndResourcePaths) { int colonPos = scriptAndResourcePath.indexOf(":"); String scriptCode = scriptAndResourcePath.substring(0, colonPos).trim(); String resourcePath = scriptAndResourcePath.substring(colonPos+1).trim(); tailored.put(UCharacter.getPropertyValueEnum(UProperty.SCRIPT, scriptCode), resourcePath); } } cjkAsWords = getBoolean(args, "cjkAsWords", true); myanmarAsWords = getBoolean(args, "myanmarAsWords", true); if (!args.isEmpty()) { throw new IllegalArgumentException("Unknown parameters: " + args); } } @Override public void inform(ResourceLoader loader) throws IOException { assert tailored != null : "init must be called first!"; if (tailored.isEmpty()) { config = new DefaultICUTokenizerConfig(cjkAsWords, myanmarAsWords); } else { final BreakIterator breakers[] = new BreakIterator[UScript.CODE_LIMIT]; for (Map.Entry<Integer,String> entry : tailored.entrySet()) { int code = entry.getKey(); String resourcePath = entry.getValue(); breakers[code] = parseRules(resourcePath, loader); } config = new DefaultICUTokenizerConfig(cjkAsWords, myanmarAsWords) { @Override public BreakIterator getBreakIterator(int script) { if (breakers[script] != null) { return (BreakIterator) breakers[script].clone(); } else { return super.getBreakIterator(script); } } // TODO: we could also allow codes->types mapping }; } } private BreakIterator parseRules(String filename, ResourceLoader loader) throws IOException { StringBuilder rules = new StringBuilder(); InputStream rulesStream = loader.openResource(filename); BufferedReader reader = new BufferedReader (IOUtils.getDecodingReader(rulesStream, StandardCharsets.UTF_8)); String line = null; while ((line = reader.readLine()) != null) { if ( ! line.startsWith("#")) rules.append(line); rules.append('\n'); } reader.close(); return new RuleBasedBreakIterator(rules.toString()); } @Override public ICUTokenizer create(AttributeFactory factory) { assert config != null : "inform must be called first!"; return new ICUTokenizer(factory, config); } }