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.
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.
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.