Feb 16, 2011

Apache Solr 1.4 Filter の作成 DigitFilter, KatakanaStemFilter

以下、POSFilter と同様にして、
DigitFilter, KatakanaStemFilterを作成します。

各 Factory クラスは、create するだけなので、非常に単純で、以下のような内容です。
public class DigitFilterFactory extends BaseTokenFilterFactory {

    public TokenStream create(TokenStream input) {
        return new DigitFilter(input);
    }
}


そして、Filter クラスは以下のような感じです。
public class DigitFilter extends TokenFilter {
    boolean preRead;
    String preTerm;
    String preType;
    int    preStart;
    int    preEnd;

    protected DigitFilter(TokenStream input) {
        super(input);
        preRead = false;
        preTerm = preType = null;
        preStart = preEnd = 0;
    }

    public Token next(Token token) throws IOException {
        if (preRead) {
            preRead = false;
            return preTerm == null ?
                    null : token.reinit(preTerm, preStart, preEnd, preType);
        }

        Token t = input.next(token);
        if (t == null)
            return null;

        char[] c;  // for termBuffer

        if (t.termLength() == 1
          && Character.isDigit((c = t.termBuffer())[0])) {
            int start = t.startOffset();
            int end   = t.endOffset();
            String type = t.type();

            StringBuilder st = new StringBuilder();
            st.append(c[0]);
            while (true) {
                t = input.next(token);
                if (t == null) {
                    preRead = true;
                    preTerm = null;
                    break;
                }
                else if (t.termLength() != 1
                  || !Character.isDigit((c = t.termBuffer())[0])) {
                    preRead  = true;
                    preTerm  = new String(c, 0, t.termLength());
                    preStart = t.startOffset();
                    preEnd   = t.endOffset();
                    preType  = t.type();
                    break;
                }
                st.append(c[0]);
                end = t.endOffset();
            }
            return token.reinit(st.toString(), start, end, type);
        }
        return t;
    }
}

これで、以下の fieldType ができました。
<fieldType name="text_ja" class="solr.TextField">
    <analyzer>
      <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ja.txt" />
      <tokenizer class="SenTokenizerFactory" />
      <filter class="POSFilterFactory" deny="pos-deny.txt" />
      <filter class="DigitFilterFactory" />
      <filter class="solr.LowerCaseFilterFactory" />
      <filter class="KatakanaStemFilterFactory" />
      <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords-ja.txt" />
    </analyzer>
  </fieldType>

これで、SysnonymFilter も追加できますね。

※ 2/18 WordJoinFilter は問題があったので削除しました。

Apache Solr 1.4 Filter の作成 POSFilter

Tokenizer を作成したので、次はFilter を作ります。

Filter も Tokenizer と同様に、Factory クラスを作成し、Filter を create します。

Factoryクラスは、「 org.apache.solr.analysis.BaseTokenFilterFactory 」を継承し、
createメソッドを実装すればOKですが、設定ファイルをロードする必要がある場合などは、
「 org.apache.solr.util.plugin.ResourceLoaderAware 」を implement して、
inform メソッドを実装します。

このあたりは、Solr 本付属のソースや、
org.apache.solr.analysis.StopFilterFactory のソースなどが参考になります。

まずは、POSFilterFactory.class と、POSFilter.class を作成します。

POSFilterFactory.class で作成する 設定ファイルから読み込む POS情報ですが、
スレッド間で同期する必要がないので、Solr 本と同様に Set で実装します。

なので、POSFilterFactory のソースの抜粋は、以下のように。
public class POSFilterFactory extends BaseTokenFilterFactory implements ResourceLoaderAware {
    private Set<string> posSet;
    public void inform(ResourceLoader loader) {
        try {
            List<string> alist = loader.getLines(denyPOSFile);
            posSet = POSFilter.makePOSSet(alist);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public TokenStream create(TokenStream input) {
        return new POSFilter(input, posSet);
    }
}

public final class POSFilter extends TokenFilter {
    private final Set<string> posSet;

    public POSFilter(TokenStream input, Set<string> posSet) {
        super(input);
        this.posSet = posSet;
    }

    public final static Set<string> makePOSSet(List<string> posList) {
        if (posList == null)
            throw new NullPointerException("posList is null");
        return new HashSet<string>(posList);
    }

    public final Token next(Token token) throws IOException {
        Token t;
        while (true) {
            t = input.next(token);
            if (t == null)
                return null;
            if (posSet == null || !posSet.contains(t.type()))
                break;
        }
        return t;
    }
}


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

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


参考本

Feb 3, 2011

Apache Solr 1.4 Sen の辞書へ単語の追加

まずカスタム辞書を用意します。

カスタム辞書は以下の形式のCSVとします。

見出し語,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音

コストは単語の発生しやすさとのことです。小さい程よく発生する単語とのことで、
3000を基準に発生しやすいものは小さく、そうでないものは大きくするといいようです。

ある単語を登録する時に、何かの複合語になっている場合は、元のそれぞれの単語が
どの程度のコストで登録されているかを辞書ファイルを検索して調べると良いのかな?

あとは、カスタム辞書を用意して、ここでやったように、
辞書のCSVファイルの所に、カスタム辞書を追記して、antを実行して辞書を再生成します。

# vi $SEN_HOME/dic/build.xml

<arg line="customize_dic.csv dic.csv" />

# ant


しかし、、sen の辞書を追加(MkSenDic)したあとに、Tomcat を再起動しないとエラーになります。
実際には先にTomcatを停止した上で辞書の再生成をすることになると思いますが、どうにか起動したままできないものか。。



参考サイト



Feb 2, 2011

Apache Solr 1.4 スキーマの設定 schema.xml fieldType

Solr のスキーマの設定は、$SOLR_HOME/conf/schema.xml に記述します。

fieldTypeの設定

<fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>


sortMissingLast / SortMissingFirst の設定

sortMissingLast="true": ソートする際に値が空の場合に検索結果の最後に空の結果を配置する。
sortMissingFirst="true": ソートする際に値が空の場合に検索結果の最初に空の結果を配置する。

デフォルトは両方ともfalse。この場合は、以下のようになります。
昇順(asc)を指定した場合は、最初に空の結果を配置
降順(desc)を指定した場合は、最後に空の結果を配置

また、両方とも true にすると、いずれの場合も最後に配置されるようになります。

全パターンで試してみた所、どうやら sortMissingLastの設定が優先されるのかな?


omitNorms は、検索にヒットした時のスコアを計算する時の設定項目です。

平均値(Norm)を省略する(omit)かどうか、みたいな値です。

平均値とは、検索ワード/フィールドの文字長 の値で、この値が大きければスコアを上げるということになります。
元になるキーワードの量によって、スコアを変えたくない場合は、true に設定します。

ただし、boost 値を利用する場合は、omitNorms="false" にしておきます。

boost値とは、ドキュメントをアップロードする際に、各フィールドの重み付けをしたり、
検索クエリで、fieldname:value^boost のようにして、検索値の重み付けをすることができます。

この辺りがどう作用しているのかは、検索クエリに、debugQuery=on としてスコアの計算を見ることができます。


デフォルトで用意されているフィールドタイプ一覧

文字列、真偽、バイナリ

string
boolean(true, false)
binary(sent/retrived as Base64 encoded Strings)

数値、日付系

int
float
long
double
date

より速い範囲検索が必要な場合は、t~ を検討。

tint
tfloat
tlong
tdouble
tdate

以下は下位互換性のためにあるものなので、基本的には利用しない。

pint
plong
pfloat
pdouble
pdate

sint
slong
sfloat
sdouble

int, tint などは、いずれも solr.TrieXxxField の実装で、トライ木による木構造でIndexが生成されます。

違いは、precisionStepの値の設定。
しかし、precisionStep を変えるとどうなるのかは、分からなかった。。

schema.xml には以下のように書いてあるから、値が小さい方が範囲検索は速そう。

Smaller precisionStep values (specified in bits) will lead to more tokens
indexed per value, slightly larger index size, and faster range queries.
A precisionStep of 0 disables indexing at different precision levels.

でも、NumericField の、javadoc を見ると、デフォルト値は4になっており、
tint などでは、8に設定してあるから、デフォルトより大きい?と思ったり。。
これは今後の調査課題としておきます。


特殊なフィールドタイプ

random(ランダムソート時に利用)
ignored(完全に無視するフィールドに利用)

その他のカスタムフィールドタイプ

text_ws
text
textTight
textgen
text_rev
lowercase
....

これらがどういう働きをするかは、schema.xml を見ると分かります。


lucene-ja を使った、形態素解析のフィールドタイプは以下のように指定します。

<fieldType name="text_ja" class="solr.TextField">
 <analyzer class="org.apache.lucene.analysis.ja.JapaneseAnalyzer" />
</fieldType>

カスタムフィールドは、全て class="solr.TextField" と指定します。


これらたいていのことは、schema.xml を読めば、ほとんど説明が書いてあります。