協作閣

開源協作部落格

使用BlackLab建立語料庫

Don / 2020-04-17 /


在尋找適合查詢PTT語料庫的系統時,查到了由Dutch Language Institute (INT)所開發的BlackLab。這個語料庫系統由Java寫成,並建在Apache Lucene的基礎之上。Apache Lucene同時也是現在很多人在用的全文搜尋系統Elasticsearch的底層架構。

之前阿吉學長曾經把PTT建在Elasticsearch裡頭,但那個時候一直不知道怎麼讓Elasticsearch也能做類似CQL (Corpus Query Language)的搜尋。而BlackLab的一大賣點就是他可以做到這一點。

從BlackLab的GitHub可以看到,最早一版的V.1.0.0 release是在2014年,而我現在所使用的V.2.0.0則是今年一月才發布的。於是想要來玩玩看試試看。但過程中遇到了一些困難,因為自己沒寫過什麼Java,所以在按照BlackLab網站的說明流程時卡關了很久,後來終於解決了,想把這個過程記錄下來當作備忘。

以下Step 1 - Step 6 是按照官方網站的Getting start的操作流程,再加上我自己在過程中的註解。

Step 1: 確認自己的電腦上有Java的JDK和JRE

JVM, JRE, JDK

Java是一個很大的生態系,光是在安裝Java的過程中,就被一大堆專有名詞給纏住。Java的生態系大致有以下這三種不可或缺的東西: - JVM (Java Virtual Machine) - JRE (Java Runtime Environment) - JDK (Java Developer Kit)

詳細內容請分別參考這個部落格: - 為什麼需要JVM? - 什麼是JRE? - 來安裝JDK

或者: - GeeksforGeeks: Differences between JDK, JRE and JVM

JAR, WAR

JAR和WAR是用來將Java寫好的code打包在一起的東西。

Step 2: 下載 BlackLab

官方的Github release頁面下載blacklab-server-2.0.0.war

然後解開.war檔:

$ java -xvf blacklab-server-2.0.0.war

應該會出現 WEB-INFMETA-INF 這兩個資料夾。

接著進去 WEB-INF 這個資料夾:

$ cd WEB-INF

然後執行以下指令,正確的話應該會跳出這個指令要求的一些參數:

$ java -cp "lib/*" nl.inl.blacklab.tools.IndexTool

會顯示:

Usage:
  IndexTool {add|create} [options] <indexdir> <inputdir> <format>
  IndexTool delete <indexdir> <filterQuery>

Options:
  --maxdocs <n>          Stop after indexing <n> documents
  --linked-file-dir <d>  Look in directory <d> for linked (e.g. metadata) files
  --nothreads            Disable multithreaded indexing (enabled by default)

Deprecated options (not needed anymore with .yaml format configs):
  --indexparam <file>    Read properties file with parameters for DocIndexer
                         (NOTE: even without this option, if the current
                         directory, the input or index directory (or its parent)
                         contain a file named indexer.properties, these are passed
                         to the indexer)
  ---<name> <value>      Pass parameter to DocIndexer class
  ---meta-<name> <value> Add an extra metadata field to documents indexed.
                         You can also add a property named meta-<name> to your
                         indexer.properties file. This field is stored untokenized.

所以要進行Indexing最主要要執行的就是:

IndexTool create [options] <indexdir> <inputdir> <format>

需要給的參數: - indexdir: indexing後的檔案要存放在哪個資料夾 - inputdir: 準備要進行indexing的語料原始檔在哪個資料夾 - format: 你的語料原始檔是什麼格式

關於format的參數,Blacklab支援很多種語料格式:(取自 官網文件 中的 Supported formats 一節) - tei (Text Encoding Initiative, a popular XML format for linguistic resources, including corpora. indexes content inside the ‘body’ element; assumes part of speech is found in an attribute called ‘type’) - sketch-wpl (the TSV/XML hybrid input format the Sketch Engine/CWB use) - chat (Codes for the Human Analysis of Transcripts, the format used by the CHILDES project) - folia (a corpus XML format popular in the Netherlands) - tsv-frog (tab-separated file as produced by the Frog annotation tool) - csv - tsv - txt - pagexml (OCR XML format) - alto (an OCR XML format) - whitelab2 (FoLiA format, but specifically tailored for the WhiteLab2 search frontend) - sketchxml (files converted from the Sketch Engine’s tab-separated format to be “true XML”, so each token corresponds to a ‘w’ tag) - di-tei-element-text (a variant of TEI where content inside the ‘text’ element is indexed) - di-tei-pos-function (a variant of TEI where part of speech is in an attribute called ‘function’)

因為每個人手上的語料需要的標記不同,所以Blacklab也提供了可以自定義欄位的方式,讓indexing的方式更能符合自己的需求。(請參考官網的How to configure indexing

Step 3: 準備好語料

以下是官方提供的荷蘭文範例語料,為TEI格式。 把這個存成文字檔,並放在一個資料夾裡面(假設是 ~/data/corpus ),這個資料夾的路徑待會會用到。

<TEI.2>
    <teiHeader>
        <fileDesc>
            <titleStmt>
                <title>Content of file grpea_0190 (converted to TEI P4
                    format)</title>
            </titleStmt>
            <publicationStmt>
                <p />
            </publicationStmt>
            <sourceDesc>
                <p>grpea 0190</p>
                <listBibl id="inlMetadata">
                    <bibl>
                        <interpGrp type="date.publication">
                            <interp value="1990-01" />
                        </interpGrp>
                        <interpGrp type="idno">
                            <interp value="5mwc.grpea_0190" />
                        </interpGrp>
                        <interpGrp type="article.class">
                            <interp
                                value="default-corpuscomponent.milieu" />
                        </interpGrp>
                        <interpGrp type="corpus.provenance">
                            <interp
                                value="INL-vijfmiljoenwoordencorpus" />
                        </interpGrp>
                        <interpGrp type="properties">
                            <interp value="medium=tijdschrift" />
                        </interpGrp>
                    </bibl>
                </listBibl>
            </sourceDesc>
        </fileDesc>
    </teiHeader>
    <text>
        <body>
            <ab>
                <lb />
                <s>
                    <w lemma="het"
                        type="VNW(pers,pron,stan,red,3,ev,onz)"
                        xml:id="w.4502">Het</w>
                    <w lemma="zien" type="WW(pv,tgw,met-t)"
                        xml:id="w.4503">ziet</w>
                    <w lemma="ernaar" type="BW()" xml:id="w.4504">ernaar</w>
                    <w lemma="uit" type="VZ(fin)" xml:id="w.4505">uit</w>
                    <w lemma="dat" type="VG(onder)" xml:id="w.4506">dat</w>
                    <w lemma="Frankrijk"
                        type="N(eigen,ev,basis,onz,stan)"
                        xml:id="w.4507">Frankrijk</w>
                    <pc type="SPEC(afgebr)" xml:id="pc.000607">,</pc>
                    <w lemma="België"
                        type="N(eigen,ev,basis,onz,stan)"
                        xml:id="w.4508">België</w>
                    <w lemma="en" type="VG(neven)" xml:id="w.4509">en</w>
                    <w lemma="Nederland"
                        type="N(eigen,ev,basis,onz,stan)"
                        xml:id="w.4510">Nederland</w>
                    <w lemma="niet" type="BW()" xml:id="w.4511">niet</w>
                    <w lemma="in" type="VZ(init)" xml:id="w.4512">in</w>
                    <lb />
                    <w lemma="staat"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4513">staat</w>
                    <w lemma="zijn" type="WW(pv,tgw,mv)"
                        xml:id="w.4514">zijn</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4515">de</w>
                    <w lemma="enorm"
                        type="ADJ(prenom,basis,met-e,stan)"
                        xml:id="w.4516">enorme</w>
                    <w lemma="milieuprobleem" type="N(soort,mv,basis)"
                        xml:id="w.4517">milieuproblemen</w>
                    <w lemma="van" type="VZ(init)" xml:id="w.4518">van</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4519">de</w>
                    <w lemma="Schelde"
                        type="N(eigen,ev,basis,zijd,stan)"
                        xml:id="w.4520">Schelde</w>
                    <w lemma="op" type="VZ(fin)" xml:id="w.4521">op</w>
                    <w lemma="te" type="VZ(init)" xml:id="w.4522">te</w>
                    <w lemma="lossen" type="WW(inf,vrij,zonder)"
                        xml:id="w.4523">lossen</w>
                    <pc type="SPEC(afgebr)" xml:id="pc.000608">,</pc>
                    <w lemma="en" type="VG(neven)" xml:id="w.4524">en</w>
                    <w lemma="dat"
                        type="VNW(aanw,pron,stan,vol,3o,ev)"
                        xml:id="w.4525">dat</w>
                    <w lemma="brengen" type="WW(pv,tgw,met-t)"
                        xml:id="w.4526">brengt</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4527">de</w>
                    <w lemma="uitvoering"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4528">uitvoering</w>
                    <lb />
                    <w lemma="van" type="VZ(init)" xml:id="w.4529">van</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4530">de</w>
                    <w lemma="afspraak"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4531">afspraak</w>
                    <w lemma="voor" type="VZ(init)" xml:id="w.4532">voor</w>
                    <w lemma="deze"
                        type="VNW(aanw,det,stan,prenom,met-e,rest)"
                        xml:id="w.4533">deze</w>
                    <w lemma="rivier"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4534">rivier</w>
                    <w lemma="in" type="VZ(init)" xml:id="w.4535">in</w>
                    <w lemma="gevaar"
                        type="N(soort,ev,basis,onz,stan)"
                        xml:id="w.4536">gevaar</w>
                    <pc type="LET()" xml:id="pc.000609">.</pc>
                </s>
                <s>
                    <w lemma="uit" type="VZ(init)" xml:id="w.4537">Uit</w>
                    <w lemma="een" type="LID(onbep,stan,agr)"
                        xml:id="w.4538">een</w>
                    <w lemma="in" type="VZ(init)" xml:id="w.4539">in</w>
                    <w lemma="opdracht"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4540">opdracht</w>
                    <w lemma="van" type="VZ(init)" xml:id="w.4541">van</w>
                    <w lemma="Greenpeace"
                        type="N(eigen,ev,basis,onz,stan)"
                        xml:id="w.4542">Greenpeace</w>
                    <w lemma="opstellen" type="WW(vd,vrij,zonder)"
                        xml:id="w.4543">opgesteld</w>
                    <w lemma="rapport"
                        type="N(soort,ev,basis,onz,stan)"
                        xml:id="w.4544">rapport</w>
                    <lb />
                    <w lemma="blijken" type="WW(pv,tgw,met-t)"
                        xml:id="w.4545">blijkt</w>
                    <w lemma="dat" type="VG(onder)" xml:id="w.4546">dat</w>
                    <w lemma="van" type="VZ(init)" xml:id="w.4547">van</w>
                    <w lemma="een" type="LID(onbep,stan,agr)"
                        xml:id="w.4548">een</w>
                    <w lemma="vermindering"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4549">vermindering</w>
                    <w lemma="van" type="VZ(init)" xml:id="w.4550">van</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4551">de</w>
                    <w lemma="vervuiling"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4552">vervuiling</w>
                    <w lemma="van" type="VZ(init)" xml:id="w.4553">van</w>
                    <w lemma="de" type="LID(bep,stan,rest)"
                        xml:id="w.4554">de</w>
                    <w lemma="Schelde"
                        type="N(eigen,ev,basis,zijd,stan)"
                        xml:id="w.4555">Schelde</w>
                    <w lemma="geen"
                        type="VNW(onbep,det,stan,prenom,zonder,agr)"
                        xml:id="w.4556">geen</w>
                    <w lemma="spraak"
                        type="N(soort,ev,basis,zijd,stan)"
                        xml:id="w.4557">sprake</w>
                    <w lemma="zijn" type="WW(pv,tgw,ev)"
                        xml:id="w.4558">is</w>
                    <pc type="LET()" xml:id="pc.000610">.</pc>
                </s>
                <lb />
            </ab>
        </body>
    </text>
</TEI.2>

Step 4: 進行Indexing

現在萬事俱備只欠東風,我們從一開始有裝了Java,下載了blacklab,準備好語料原始檔,也確定語料的格式是TEI之後,就可以開始準備進行Indexing:

還記得剛剛的指令嗎?

$ java -cp "lib/*" nl.inl.blacklab.tools.IndexTool

現在我們要把其他參數也填進去:

$ $ java -cp "lib/*" nl.inl.blacklab.tools.IndexTool create ~/data/indexed_file ~/data/corpus tei-p4

成功的話你應該會看到:

1 docs (8 kB, 57 tokens); avg. 0.2k tok/s (0.0 MB/s); currently 0.2k tok/s (0.0 MB/s); 378 ms elapsed
Done. Elapsed time: 0 seconds

也就是1個document已經成功被index了。然後你就會在你剛剛所指定的index存放資料夾(以我的例子是:~/data/indexed_file)看到裡面多了很多檔案。

Step 5: 實際進行Query

既然成功Indexing後,就代表我們可以對這個indexed corpus進行Query了。

blacklab提供兩種query方式,第一種是command line,第二種是透過架一個 blacklab-server 的 HTTP API。

這裡介紹第一種:

$ $ java -cp "lib/*" nl.inl.blacklab.tools.QueryTool <index所在的資料夾>

這個指令只要求一個參數,也就是你index後的檔案所存放的資料夾,所以我就要輸入:

$ $ java -cp "lib/*" nl.inl.blacklab.tools.QueryTool~/data/indexed_file

成功輸入後你會看到指令列跳出一個類似互動式的程式,會跳出CorpusQL的prompt,你就可以按照他跳出的說明來query,當然也可以使用CQL,例如:

CorpusQL> [][word="de"]

就會看到結果:

   1. [0000]           en Nederland niet in staat [zijn de] enorme milieuproblemen van de Schelde
   2. [0000] staat zijn de enorme milieuproblemen [van de] Schelde op te lossen , en
   3. [0000]                op te lossen , en dat [brengt de] uitvoering van de afspraak voor
   4. [0000]          en dat brengt de uitvoering [van de] afspraak voor deze rivier in
   5. [0000]      blijkt dat van een vermindering [van de] vervuiling van de Schelde geen
   6. [0000]   een vermindering van de vervuiling [van de] Schelde geen sprake is
6 hits in 1 documents
33 ms elapsed

當然command line並沒有那麼方便,使用第二種方法,也就是用HTTP API的方式會比較好用,這個可以下次再寫一下如何使用,要搭配Java的Tomcat server一起使用。

以下的連結是我自己練習用的,把PTT婚姻版(marriage)所有的語料經過上面的Indexing流程後,再使用blacklab的前後端所架設的一個搜尋頁面,大家可以玩看看: http://140.112.147.125:8887/corpus-frontend/ptt_marriage_index/search

之後應該會試著把所有PTT的語料都搬到這上面,測試看看那麼大量的資料是否也可以快速檢索。