Feb 16, 2011

Apache Solr 1.4 Tokenizerの作成

JapaneseAnalyzer は、その名のとおり Analyzer クラ なので、schema.xml で細かなフィルターの設定はできません。
そこで、Tokenizerクラスを作成し、Filter、charFilter を柔軟に設定できる fieldType を作成します。

形態素解析には、同様に Sen を使用することとします。

TokenizerFactory を作る時は、「 org.apache.solr.analysis.BaseTokenizerFactory.class 」を実装し、
Factoryメソッドで、Tokenizer を作成します。
この辺りの流れは、ドキュメントを見るか、実際の solr や lucene のソースを見るのが参考になります。

JapaneseAnalyzer は、以下の流れで解析しています。

Reader を、NormalizeReader でラップし、全角⇔半角に文字を寄せる。
設定フィルで記述した tokenizerClass のインスタンスを作成。
POSFilter - 不要な型の term を除去
DigitFIlter - Senによって分割された数字を結合
LowerCaseFilter - 大文字を小文字に変換
KatakanaStemFilter - 4文字以降のカタカナの最後の「ー」を除去
StopFilter - stopwords を削除

最終的にはこのような fieldType を作成することを目標に、まずは、Tokenizerを作成します。

JapaneseAnalyzerを使うときは、システムプロパティからsen.home の値を取得しますが、
Solr 入門の本や、org.apache.solr.core.SolrResourceLoader.class の様に、
「 JNDI -> System Property -> デフォルト 」
とチェックするのがいいと思うので、そのように実装し、
sen/home は web.xml に記述することにします。

ただし、JapaneseAnalyzer を使う場合は、必ずシステムプロパティが必要なので、
同じ設定を2箇所に記述するよりは、システムプロパティに統一するほうがよさそうです。

そんなこんなで、Factoryクラスのソースは以下のような感じになります。
public class SenTokenizerFactory extends BaseTokenizerFactory {
  
    private static final Logger log
                    = LoggerFactory.getLogger(SenTokenizerFactory.class);

    static final String PROP_SEN_HOME = "sen.home";
    static final String JNDI_SEN_HOME = "sen/home";
    static final String FS = System.getProperty("file.separator");
    static final String SEN_XML = FS + "conf" + FS + "sen.xml";

    String configFile;
    String compositRule;

    @Override
    public void init(Map args) {
        String senHome = null;

        // Try JNDI
        try {
            Context c = new InitialContext();
            senHome = (String)c.lookup("java:comp/env/" + JNDI_SEN_HOME);
            log.info("Using JNDI sen/home: " + senHome);
        } catch (NoInitialContextException e) {
            log.info("JNDI not configured for Solr (NoInitialContextEx)");
        } catch (NamingException e) {
            log.info("No sen/home in JNDI");
        } catch (RuntimeException ex) {
            log.warn("Odd RuntimeException while testing for JNDI: " 
                    + ex.getMessage());
        } 

        // Now try system property
        if (senHome == null){
            senHome = System.getProperty(PROP_SEN_HOME);
            log.info("Using System property sen.home: " + senHome);
        }

        // Set current path
        if (senHome == null) {
            senHome = ".";
            log.info("sen.home defaulted to '.' (could not find system property or JNDI)");
        }

        configFile = senHome + SEN_XML;

        log.info( "config file for SenTokenizer is " + configFile );

        readConfig();

        log.info("conpositRule is: "
                + (compositRule == null ? "NULL" : compositRule));

    }

    protected String getConfigFile() {
        return configFile;
    }

    private void readConfig() {
        List<String> compositRuleList = new ArrayList<String>();

        try {
            DocumentBuilderFactory factory
                            = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(configFile));
            NodeList nl = doc.getFirstChild().getChildNodes();

            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Node n = nl.item(i);
                if (n.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                    String nn = n.getNodeName();
                    String value = n.getFirstChild().getNodeValue();

                    if (nn.equals("composit")) {
                        compositRuleList.add(value);
                        log.info("add composit rule: " + value);
                    }
                }
            }

            if (compositRuleList.size() > 0) {
                compositRule = StringUtils.join(compositRuleList, "\n");
            }

        } catch (ParserConfigurationException e) {
            throw new IllegalArgumentException(e.getMessage());
        } catch (FileNotFoundException e) {
            throw new IllegalArgumentException(e.getMessage());
        } catch (SAXException e) {
            throw new IllegalArgumentException(e.getMessage());
        } catch (IOException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

    public Tokenizer create(Reader input) {
        try {
            return new SenTokenizer(input, configFile, compositRule);
        } catch (IOException e) {
            throw new RuntimeException("cannot initialize SenTokenizer: "
                                        + e.toString());
        }
    }
}

そして、SenTokenizerクラスは以下のように。
public class SenTokenizer extends Tokenizer {

    private StreamTagger tagger       = null;
    private String       configFile   = null;
    private String       compositRule = null;

    private static final HashSet hash = new HashSet();

    public SenTokenizer(Reader input, String configFile, String compositRule)
            throws IOException {
        super(input);
        this.configFile = configFile;
        this.compositRule = compositRule;
        init(input);
    }
    
    private void init(Reader input) throws IOException {
        tagger = new StreamTagger(input, configFile);

        synchronized(hash) {
            if (compositRule != null && !compositRule.equals("")) {
                if (!hash.contains(compositRule)) {
                    CompositPostProcessor p = new CompositPostProcessor();
                    p.readRules(new BufferedReader(new StringReader(
                            compositRule)));
                    hash.add(compositRule);
                    tagger.addPostProcessor(p);
                }
            }
        }
    }

    public Token next(Token token) throws IOException {
        if (!tagger.hasNext())
            return null;

        net.java.sen.Token t = tagger.next();

        if (t == null)
            return next(token);

        return token.reinit(
                t.getBasicString(),
                correctOffset(t.start()),
                correctOffset(t.end()),
                t.getPos());
    }

    @Override
    public void reset(Reader input) throws IOException {
        super.reset(input);
        init(input);
    }
}

これで、fieldType の以下の部分までができました。
<fieldType name="text_ja" class="solr.TextField">
    <analyzer>
      <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ja.txt" />
      <tokenizer class="mydomain.SenTokenizerFactory" />
    </analyzer>
  </fieldType>


追記:
2/18 sen.xml に、composit の設定を追加した際の処理をソースに追記しました。
まだマルチスレッドでうまく動作するかはテスト中です。



参考サイト

http://d.hatena.ne.jp/bowez/20090513


参考本

No comments:

Post a Comment