Jakarta Lucene keresőmotor

Viczián István (viczus@freemail.hu)

Ez a cikk azon gyakorlott Java programozóknak szól, akiknek szükségük van teljes szövegben való keresési lehetőségre, akár különálló programban, akár egy teljes webes alkalmazásban. A Lucene egy nagyteljesítményű, minden alkalmazási területet lefedő Java nyelven implementált keresőmotor, mely 2001-ben csatlakozott a Jakarta projekthez, ezért szabad forrású, ingyenesen használható (Apache Software Licence).

A Lucene keresőmotor alapvetően dokumentumokat kezel, melyek részhalmazát ki kell tudni választani egy keresési feltétel alapján. A dokumentumot mezőkre bontja. A keresésként kapott dokumentumokat rangsorolja, mely megadja, hogy a keresési feltételnek mennyire felel meg egy adott dokumentum. A keresést az index gyorsítja, ami tulajdonképpen egy adatbázis (fájlok), melybe előfeldolgozott információk találhatók a dokumentumokra vonatkozóan. A dokumentum mezőire külön megadható, hogy indexelődjenek-e vagy sem. Az indexelést feldolgozók végzik, melyből több, eltérő funkcionalitású is rendelkezésre áll. A keresési feltétel lehet egy szó, kifejezés vagy akár ezekkel megadott bonyolult logikai feltétel. A dokumentumoknak dátum is adható, így lehetséges a keresési feltételben egy időintervallumot is megadni.

Lucene logó

A Lucene magját Doug Cutting írta, de mára többen csatlakoztak a fejlesztéshez. A cikk megírása pillanatában az aktuálisan elérhető legújabb verzió a Lucene 1.3 RC1, mely 2003. március 23-án jelent meg. Ettől függetlenül ez a cikk bevezető jellegű, verzió specifikus elemeket minimális mértékben tartalmazhat.

A Lucene semmilyen más könyvtárakra nem tartalmaz hivatkozást, elegendő a JAR fájlt (jelenlegi verzióban ennek neve lucene 1.3 rc1.jar) a CLASSPATH környezeti változóban elhelyezni.

Tegyük fel, hogy van egy cd lemezeket kezelő alkalmazásunk, melynek egyik osztálya a Disc, mely egy cd lemez reprezentál. Az objektum myId, myArtist, myTitle, myDate és myTracks attribútumai jelzik rendre a cd lemez egyedi azonosítóját, előadóját, címét, kiadás dátumát és a számok címét. A következő UML diagram ábrázolja a cd lemezt.

Disc objektum

Ahhoz, hogy egy dokumentumot tárolhassunk, a következő importhivatkozásokat kell alkalmazni:

 
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.DateField;

A következő metódus indexel egy cd lemezt, pontosabban annak csak a címét:

 
    /**
     * Indexel egy cd lemezt.
     * @param pDisc lemez, melyet indexelni kell
     * @throws java.io.IOException kivétel váltódik ki, ha az 
     * index állományok nem írhatóak
     */
    public void index(Disc pDisc) throws java.io.IOException {
         Analyzer anAnalyzer = new StandardAnalyzer();
         IndexWriter anIndexWriter = new IndexWriter("./index", anAnalyzer, true);              
         Document aDocument = new Document();
         aDocument.add(Field.Text("title", pDisc.getTitle()));
         anIndexWriter.addDocument(aDocument);                                                             
         anIndexWriter.close();
    }

A kódrészlet létrehoz egy beépített szabványos feldolgozót (Analyzer), létrehoz egy IndexWriter objektumot és egy üres dokumentumot (Document). Az IndexWriter konstruktorának első paramétere az index fájlok helye, a második az előbb létrehozott elemző, a harmadik meghatározza, hogyha létezik-e már index, akkor felülírja-e azt, vagy adja hozzá a már meglévőkhöz a következő dokumentumokat. A dokumentumhoz hozzáad egy mezőt, "title" névvel és a valós tartalommal. Majd a dokumentumot átadja az IndexWriter-nek. A metódus IOException kivételt dobhat, ha az index fájlok valamilyen okból nem írhatóak.

Jelenleg egy beépített elemzőt használtunk StandardAnalyzer névvel, de válogathatunk a többi közül (GermanAnalyzer, RussianAnalyzer, SimpleAnalyzer, StandardAnalyzer, StopAnalyzer, WhitespaceAnalyzer), vagy akár sajátot is implementálhatunk, melynek az Analyzer interfészt kell megvalósítania.

A Text típusú mezőn kívül (, melyet az elemző szavakra bont, és azokat indexeli), használhatunk Keyword típusút (, melyet nem bont szavakra, de indexel, tipikusan telefonszámoknak, e-mail címeknek). Ezen kívül létezik Unindexed mező, melyet nem indexel, de eltárolja az indexben (nem kereshető, de a keresés eredményének megjelenítésekor ez is megjelenik), illetve az Unstored mező, melyet indexel ugyan, de nem jelenik meg a keresés eredményében.

Text mezőnek nem csak String típusú objektumot lehet megadni, hanem akár bármilyen Reader objektumot, ilyenkor azonban nem tárolódik az indexben.

Ha tehát azt akarjuk, hogy az előadó nevének csak egy szavára ne lehessen külön rákeresni, akkor alkalmazzuk a következő kódsort:

 
aDocument.add(Field.Keyword("artist", pDisc.getArtist()));

A számok címére lehessen keresni, de ne tárolódjanak az indexben, hiszen nem akarjuk a keresés eredményeként megjeleníteni.

 
aDocument.add(Field.Text("tracks", pDisc.getTracks()));

Gyakran szükséges, hogy a keresés után visszakeressük az eredeti objektumot, ilyenkor érdemes eltárolnunk az objektum egyedi azonosítóját is a dokumentumban, ami szintén nem kereshető.

 
aDocument.add(Field.UnIndexed("id", Integer.toString(pDisc.getId())));

A következő táblázat megadja, hogy mely mezők esetén van szavakra bontás, indexelés, és tárolás az indexben.

Mező típus

Szavakra bontás

Indexelés

Indexben tárolás

Field.Keyword(String, String)

Nem

Igen

Igen

Field.UnIndexed(String, String)

Nem

Nem

Igen

Field.UnStored(String, String)

Igen

Nem

Nem

Field.Text(String, String)

Igen

Nem

Igen

Field.Text(String, Reader)

Igen

Nem

Nem

Abban az esetben, ha szeretnénk később a dátum szerint is feltételeket megadni, akkor azt speciális formában, a következőképpen kell tárolni:

 
aDocument.add(Field.Keyword("date",DateField.timeToString(pDisc.getDate().getTime())));

Kereséskor a következő importhivatkozásokat kell alkalmazni:

 
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Query;
import org.apache.lucene.queryParser.ParseException;

A keresésre a következő metódus szolgál:

 
    /**
     * Keres cd lemezek között.
     * @param pQuery szöveges keresési feltétel
     * @throws java.io.IOException kivétel, ha az index 
     * fájlokat nem lehet olvasni
     * @throws ParseException kivétel, ha az index fájlokat 
     * nem lehet olvasni
     */
          public void find(String pQuery) throws java.io.IOException,  ParseException {                  
                          IndexReader anIndexReader = IndexReader.open("./index");
                 IndexSearcher anIndexSearcher = new  IndexSearcher(anIndexReader);
                 Query aQuery = QueryParser.parse(pQuery, "tracks",  new StandardAnalyzer());
                 Hits hits = anIndexSearcher.search(aQuery);
 
                 int i;
                 for (i = 0; i < hits.length(); i++) {                                 
                          Document aDocument = hits.doc(i);
                          System.out.println(aDocument.get("title") + "\t" + hits.score(i));
                 }
                 anIndexSearcher.close();
         }

Először egy IndexReader objektumot, melynek paraméternek meg kell adni az index fájlok helyét, majd egy IndexSearcher objektumot, melynek az előbb létrehozott IndexReader-t kell megadni, majd egy Query-t kell létrehozni a szöveges keresési feltétel elemzésével (ez dobhat ParseException kivételt). Ennek meg kell adni a keresési feltétel szöveges ábrázolását, az alapértelmezett oszlopot és egy feldolgozót. Vigyázni kell, ha nem ugyanabba az osztályba tartozó feldolgozót használjuk indexelésnél és keresésnél, ugyanis akkor a keresés sikertelen lehet. Majd a keresést futtatva egy Hits objektumot kapunk vissza, amitől le lehet kérdezni a dokumentumokat és azok pontszámait. Ha az index fájlok nem olvashatóak, akkor IOException kivételt kapunk. A dokumentum egy mezőjét a Document.get(String fieldName) metódussal lehet lekérni.

Abban az esetben, ha egy dokumentumot törölni akarunk, akkor használjuk a IndexReader.delete(int docNum) metódust.

A keresési feltétel megadásakor megkülönböztetünk termeket (szó, pl. Jewel) és kifejezéseket (szókapcsolatok, pl. "This way"). A keresési feltételben megadhatjuk, hogy mely mezőben szeretnénk keresni, ekkor kettősponttal kell minősíteni (pl. artist:Jewel). Megengedettek a joker karakterek is, melyekkel egy vagy több karaktert lehet helyettesíteni ("?" és "*"). Fuzzy keresésre is lehetőség van a tilde ("~") karakter használatával. Meg lehet adni, hogy több szó esetén, a szavak maximum milyen távolságban helyezkedhetnek el egymástól ("this way"~10). Lehetőség van intervallum megadására is, ekkor a rendezési elv a névsorrend (pl. title:[Jewel TO Nightwish]. A szavakat és szókapcsolatokat súlyokkal láthatjuk el (pl. Jewel^4 Spirit). Logikai operátorként használható az OR, AND NOT, illetve a "+" jel jelenti, hogy a szónak mindenképp szerepelnie kell egy mezőben, a "-" jel, hogy nem szerepelhet. Lehetőség van csoportosításra is a kerek zárójelekkel, és escape szekvenciák is alkalmazhatóak speciális karakterekre.

A keresés gyorsítása érdekében lehetőség van az indexek memóriában tartására is, illetve az IndexWriter.optimize() metódushívás optimalizálja az adatbázist.

Mint ahogy a legtöbb Jakarta project keretében fejlesztett alprojektről elmondható, a Lucene is tartalmazza a teljes forráskódot, részletes dokumentációt (JavaDoc API dokumentációval), példaprogramokat és teszteseteket is.

Budapest, 2003. május 10.

Hivatkozások

The Apache Software Foundation
The Jakarta Project
Lucene