slides lucene

35
 Il progetto Apache Lucene Antonino Freno [email protected] Corso di Basi di Dati Multimediali – A.A. 2008/2009

Upload: umberto-agosto

Post on 22-Jul-2015

45 views

Category:

Documents


0 download

TRANSCRIPT

Il progetto Apache LuceneAntonino [email protected] di Basi di Dati Multimediali A.A. 2008/2009

Lidea

Lucene ` una libreria ad alte prestazioni che implementa e funzioni di indicizzazione e ricerca per documenti di testo; Le funzioni di ricerca sono eseguite sfruttando un indice inverso dei documenti, che la libreria stessa costruisce in una fase preliminare; La qualit` dellindice ` la chiave dellecienza di Lucene nella a e gestione delle interrogazione degli utenti.

Loggetto Document

In Lucene, la struttura dati fondamentale per lindicizzazione e la ricerca ` data dalloggetto Document: e

Lindice contiene un insieme di Document; Lindicizzazione consiste nellaggiungere dei Document a un IndexWriter; La ricerca consiste nel recuperare dei Document dallindice invocando un IndexSearcher.

Nel senso tecnico, i Document di Lucene non coincidono con i documenti nel senso generico delle unit` di testo contenute a nel database originario. Ad esempio:

Se si volesse indicizzare la tabella degli utenti di un database, per ciascun utente si costruirebbe un oggetto Document.

Loggetto Field

Un oggetto Document consiste di uno o pi` oggetti Field; u Un oggetto Field ` semplicemente una coppia nomevalore, e dove entrambi sono tipicamente degli oggetti String. Ad esempio, un nome tipico pu` essere title, e un valore The o Art of Computer Programming;

Per indicizzare un database, si creano dal database degli oggetti Document con dei Field opportuni, e si aggiungono i Document allIndexWriter; Una volta che lindice ` stato costruito (e scritto su RAM o su e disco), per eettuare una ricerca:1. Si crea un oggetto Query (di solito usando un QueryParser); 2. Si passa la Query a un IndexSearcher; 3. Si prende indietro dallIndexSearcher una lista di risultati.

Interrogazioni

Lucene ha un proprio mini-linguaggio per eseguire interrogazioni; Per esempio, ` possibile: e

Specicare dentro quali campi dei documenti la ricerca deve essere eettuata; Attribuire un peso maggiore ad alcuni campi piuttosto che ad altri; Utilizzare operatori booleani per comporre una query da pi` u termini.

Progettazione di unapplicazione (I)1. Determinare come si desidera appaia la pagina dei risultati di una ricerca. Ad esempio:

Che informazione deve contenere ciascun risultato? In base a quali criteri devono essere ordinati i risultati?

2. In base al punto precedente, determinare cosa occorre per far passare le informazioni rilevanti dalla collezione di testi considerata a Lucene. In particolare:2.1 Quanti e quali oggetti Field devono essere contenuti in ciascun Document? 2.2 In che modo si possono estrarre le informazioni desiderate dal formato in cui sono rappresentati i documenti della collezione? (Ad esempio, se la collezione ` immagazzinata in un database e SQL, quali interrogazioni SELECT occorre eseguire?)

3. Realizzare il modulo per lindicizzazione:3.1 Scrivere il codice per lindicizzazione; 3.2 Vericare la correttezza dellindice creato dal modulo.

Progettazione di unapplicazione (II)

4. Realizzare il modulo per la ricerca:4.1 Completare il nucleo del modulo: presa in input uninterrogazione in forma di stringa, si restituisce in output una lista (ordinata) di risultati (rilevanti); 4.2 Ranare il modulo: numero di risultati per pagina, navigazione tra pagine, peso dei diversi campi dei documenti, ecc.

5. Se necessario, aggiungere funzionalit` di ricerca aggiuntive: a5.1 5.2 5.3 5.4 Filtro dei risultati in base ai permessi di lettura; Restrizioni sul contenuto; Ordinamento per campi particolari; ...

6. Testare la qualit` dei risultati prodotti. a

Loggetto IndexWriter

Un IndexWriter si pu` costruire con listruzione onew IndexWriter(String path, Analyzer a, boolean c, IndexWriter.MaxFieldLength mfl),

dove:

path ` il percorso della directory dentro cui verr` scritto e a lindice; a ` loggetto Analyzer che si preoccuper` di analizzare il testo e a dei documenti; c stabilisce se la directory dovr` essere creata se inesistente o a sovrascritta se esistente (true), oppure soltanto arricchita di nuovi contenuti (false); mfl limita la dimensione massima dei campi dei documenti.

La classe IndexWriter.MaxFieldLength

IndexWriter.MaxFieldLength ` una classe statica innestata e utilizzata dagli oggetti IndexWriter per limitare il numero di termini indicizzati allinterno di un campo di documento; Dato un intero che indichi quel limite, lIndexWriter continua a indicizzare il contenuto di un campo no a che non lo ha raggiunto, e ignora tutto il contenuto successivo; Per impostare il limite:

IndexWriter.MaxFieldLength.LIMITED imposta il limite come lintero IndexWriter.DEFAULT_MAX_FIELD_LENGTH, ossia 10.000; IndexWriter.MaxFieldLength.UNLIMITED imposta il limite come la costante Integer.MAX_VALUE; new IndexWriter.MaxFieldLength(int limit) imposta un qualsiasi intero limit.

Preparazione degli oggetti Document

Per costruire un oggetto Document, si chiama il costruttore (senza parametri) Document; Per aggiungere contenuto al documento, si applica alloggetto (tante volte quanti sono i campi da indicizzare) il metodo addField(Fieldable field):

Fieldable ` uninterfaccia implementata dalloggetto Field; e Se doc ` un oggetto Document e f ` un oggetto Field, e e lutilizzo tipico di addField ` doc.addField(f); e Per costruire un oggetto Field, servono essenzialmente due parametri:

Il nome del campo, che ` un oggetto di tipo String; e Il valore del campo, che di solito ` una stringa o uno stream di e lettura da un le.

Costruzione di un oggetto Field: EsempiEs. 1 Si vuole costruire un oggetto Field per lintero conenuto testuale di un le:

Se fr ` un oggetto FileReader (dunque uno stream di lettura e per il le), e contenuto ` il nome che vogliamo dare al campo, e basta usare listruzione new Field(contenuto, fr).

Es. 2 Si vuole costruire un oggetto Field per il percorso del le:1. Se p ` la variabile di tipo String che contiene il percorso del e le, si usa listruzione new Field(percorso, p, Field.Store.YES, Field.Index.NOT_ANALYZED):

Field.Store e Field.Index sono due classi statiche innestate usate dentro Field; Field.Store.YES specica che il valore del campo deve essere memorizzato per intero dentro lindice; Field.Index.NOT_ANALYZED specica che il valore del campo non deve essere elaborato dallAnalyzer.

Scrittura dellindice

Prima di scrivere lindice (su disco, o anche su RAM), occorre aggiungere allIndexWriter tutti gli oggetti Document preparati in precedenza. Se writer ` loggetto IndexWriter, e un documento doc si aggiunge con una semplice istruzione writer.addDocument(doc); Lucene eettua la scrittura dellindice quando viene eseguita listruzione writer.close(), che dunque pu` essere o unoperazione anche molto costosa; Opzionalmente, prima di chiudere (scrivere) lIndexWriter, si pu` invocare loperazione writer.optimize(), che ottimizza o lindice per ottenere maggiore ecienza in fase di ricerca. Conviene eseguire lottimizzazione solo quando non si prevedono dei cambiamenti/aggiornamenti nellindice per un periodo di tempo sucientemente lungo.

Ricerca di documenti (I)

Per recuperare dei risultati dallindice, occorre usare un IndexSearcher e un oggetto Query; Se la stringa path ` il percorso (il nome della cartella) sotto e cui ` immagazzinato lindice, un IndexSearcher si pu` e o costruire con listruzione

IndexSearcher searcher = new IndexSearcher(path);

Se la stringa f ` il nome del campo di default in cui vogliamo e eseguire la ricerca e queryString ` la stringa inserita come e query, un oggetto Query si pu` costruire come segue: o1. Analyzer analyzer = new StandardAnalyzer(); 2. QueryParser parser = new QueryParser(f, analyzer); 3. Query query = parser.parse(queryString);

Loggetto analyzer deve essere dello stesso tipo di quello usato per costruire lIndexWriter!

Ricerca di documenti (II)

Per raccogliere i risultati della ricerca si usa un TopDocCollector, che ` una sorta di contenitore dei risultati. e Se lintero maxHits ` il numero massimo di risultati che e vogliamo ottenere, ci occorrono le seguenti righe di codice:1. TopDocCollector collector = new TopDocCollector(maxHits); 2. searcher.search(query, collector); 3. ScoreDoc[] hits = collector.topDocs().scoreDocs;

Preso li -esimo ScoreDoc nella lista hits, per ottenere (come stringa) il valore del campo f del rispettivo documento bastano le seguenti istruzioni:1. int docId = hits[i].doc; 2. Document document = searcher.doc(docId); 3. String fieldValue = document.get(f);

Uso del Query Parser (I)

Lucene fornisce sia la possibilit` di creare direttamente degli a oggetti di tipo Query attraverso la sua API, sia la possibilit` a di generare una Query eseguendo il parsing di una stringa inserita da un utente umano; La classe QueryParser ` quella che si occupa del parsing delle e interrogazioni inserite (a mano) dagli utenti dellapplicazione; Il Query Parser non ` lo strumento naturale per produrre e oggetti Query da stringhe generate meccanicamente, ossia prodotte in modo sistematico dalle routine della propria applicazione; Per la produzione automatica di oggetti Query ` consigliato e usare direttamente le funzioni dedicate della API, piuttosto che applicare il parser a una stringa prodotta in automatico.

Uso del Query Parser (II)

In unapplicazione tipica, lo standard ` quello di utilizzare il e Query Parser per elaborare i campi di testo libero, e le altre funzioni della API per generare (porzioni di) query dai campi con un insieme vincolato di valori; Si confronti, ad esempio, il campo di testo semplice nellinterfaccia generica di un motore di ricerca con il modulo per la ricerca avanzata:

Uso del Query Parser (III)

Mentre nel caso di una ricerca generica il testo inserito viene passato interamente ad un parser, nel caso di un modulo per la ricerca avanzata ciascun campo viene elaborato da una routine dedicata, componendo poi i risultati in una query opportuna:

Uso del Query Parser (IV)

Dal momento che i due esempi precedenti rappresentano la stessa query, ossia il termine java limitatamente al dominio en.wikipedia.org/wiki/, i risultati prodotti sono gli stessi:

Sintassi del Query Parser: termini e operatoriUna query contiene (una o pi` occorrenze di) due tipi di u stringhe: termini e operatori; I termini possono essere di due tipi: termini singoli ed espressioni:

I termini singoli sono singole parole (ad es. Java); Le espressioni sono gruppi monolitici di termini singoli racchiusi dalle doppie virgolette (ad es. "il linguaggio Java").

Gli operatori servono a combinare insieme pi` termini per u costruire query complesse; Dal momento che il Query Parser contiene un oggetto di tipo Analyzer, occorre assicurarsi che lAnalyzer usato sia appropriato al tipo di query che ci si aspetta di ricevere.

Sintassi del Query Parser: campi

Lucene supporta le query ristrette a campi particolari dei documenti; Il (nome del) campo dentro cui ciascun termine della query deve essere cercato pu` essere sia specicato esplicitamente, o sia lasciato uguale a un valore di default:

Per specicare il campo del termine, si antepone il nome del campo al termine, usando i due punti come separatore. Ad esempio, la query title:programming chiede di cercare il termine programming nel campo di nome title; Il campo di default ` quello specicato come parametro nel e costruttore del Query Parser. Se ad esempio il campo di default ` quello chiamato title, la query programming sar` e a equivalente alla query title:programming.

Sintassi del Query Parser: caratteri jolly

Lucene supporta due tipi di caratteri jolly allinterno delle query:

Il simbolo ? sta per un singolo carattere arbitrario. Ad esempio, te?t sta per test, text, ecc.; Il simbolo * sta per una serie (di lunghezza arbitraria) di simboli arbitrari. Ad esempio, test* sta per test, tester, testing, ecc.

I caratteri jolly non si possono usare come primi caratteri di una query; I caratteri jolly si possono usare solo allinterno di termini singoli, non allinterno di espressioni.

Sintassi del Query Parser: ricerche fuzzy

Lucene permette anche di eseguire delle ricerche sfumate, dove cio` si cercano anche risultati che contengano termini e simili a quelli usati nella query; Per eseguire una ricerca fuzzy, basta aggiungere il simbolo ~ alla ne di un termine singolo. Per esempio, la query lago~ restituir` non solo documenti contenenti il termine lago, ma a anche documenti che contengano uno dei termini lato, vago, largo, ecc.; E possibile specicare il grado di similarit` richiesto a aggiungendo un parametro (di valore compreso tra 0 e 1) al termine fuzzy (ad es. lago~0.8), dove 1 signica similarit` a massima, e 0 signica similarit` nulla. Il valore di default a usato da Lucene ` 0.5. e

Distanza di Levenshtein

Il grado di somiglianza tra stringhe ` misurato da Lucene e utilizzando la distanza di Levenshtein; La distanza di Levenshtein ` una metrica comunemente usata e per calcolare il grado di somiglianza tra stringhe. Lidea di base ` che la distanza tra due stringhe sia data dal numero e minimo di operazioni necessarie a trasformare una stringa nellaltra (edit distance); Lintroduzione di questa distanza (nel 1965) si deve a Vladimir Levenshtein; ` spesso usata a tuttoggi in applicazioni che e lavorano su documenti di testo, ad esempio negli spell checker.

Algoritmo per il calcolo della distanza di Levenshtein (I)

int EditDistance(char s[1...m], char t[1...n]) // creiamo una matrice (m+1)*(n+1) 1. declare int d[0..m,0..n] /* assegniamo come valore alla prima cella di ogni riga/colonna lindice della riga/colonna */ 2. for i from 0 to m 3. 5. d[i,0] = i d[0,j] = j 4. for j from 0 to n

Algoritmo per il calcolo della distanza di Levenshtein (II)// compiliamo il resto della matrice 6. for i from 1 to m 7. 8. 9. 10. 11. 12. for j from 1 to n if s[i] = t[j] cost = 0 else cost = 1 d[i,j] = min( d[i-1,j]+1, // inserimento d[i,j-1]+1, // cancellazione d[i-1,j-1]+cost // sostituzione )

Algoritmo per il calcolo della distanza di Levenshtein (III)/* restituiamo in output il valore contenuto nellultima riga dellultima colonna */ 13. return d[m,n]

Esempio (distanza di Levenshtein tra i termini casa e carta):0 1 2 3 4 C 1 0 1 2 3 A 2 1 0 1 2 R 3 2 1 1 2 T 4 3 2 2 2 A 5 4 3 3 2

C A S A

Come si vede dalla ultima riga dellultima colonna, EditDistance("casa", "carta") = 2.

Sintassi del Query Parser: ricerche di prossimit` a

Lucene supporta anche ricerche di prossimit`, cio` ricerche in a e cui si chiede che due termini arbitrari compaiano entro una certa distanza luno dallaltro; Per eseguire una ricerca di prossimit`, basta legare i due a termini in unespressione (virgolettata) e aggiungere una tilde seguita dalla distanza massima desiderata (in forma di intero):

Ad esempio, la query "apache lucene"~10 cercher` a documenti in cui i termini apache e lucene compaiono entro una distanza di al massimo 10 parole luno dallaltro.

Sintassi del Query Parser: ricerche in un intervallo (I)

E possibile eseguire ricerche su campi il cui valore sia compreso tra un limite inferiore e un limite superiore (ad esempio in relazione ad intervalli di tempo); Per specicare il range di riferimento, basta racchiudere il limite inferiore e quello superiore tra parentesi quadre (per un range che include i limiti) o tra parentesi grae (per un range che esclude i limiti), separando i limiti con loperatore TO. Ad esempio:

created_on:[20090312 TO 20090324] cerca documenti il cui campo created_on ha un valore compreso tra il 12 e il 24 marzo 2009, incluse quelle date; created_on:{20090312 TO 20090324} esegue la medesima ricerca, escludendo per` dai risultati il 12 e il 24 marzo. o

Sintassi del Query Parser: ricerche in un intervallo (II)

Lordinamento a cui si riferisce lo spettro desiderato ` un e ordinamento lessicograco (del tipo di quello di un dizionario). Ci` ha alcune conseguenze importanti: o

Le date devono essere rappresentate in un formato in cui ci sia corrispondenza tra ordine temporale e ordine lessicograco, altrimenti si ottengono risultati incoerenti. Ad esempio, se rappresentiamo una data come gg/mm/aaaa (giorno/mese/anno), accade che 01/03/2009 venga prima di 02/03/2008; E possibile cercare dei range entro qualsiasi genere di campo. Ad esempio, la query author:[brin TO page] otterr` come a risultati anche documenti il cui campo author ha valore knuth.

Operatori booleani

Le funzioni booleane supportati da Lucene sono AND, OR, NOT; Loperatore di default usato da Lucene quando lutente non ne specica alcuno ` la disgiunzione (OR). Questo signica che le e query apache lucene e apache OR lucene sono equivalenti; Per la congiunzione si pu` usare AND o &&, per la disgiunzione o OR o ||, per la negazione NOT o ! (o anche -); Per specicare lordine di applicazione degli operatori in una query complessa si usano le parentesi tonde; Loperatore + (immediatamente precedente un termine) indica che i risultati devono necessariamente contenere il termine specicato. Ad esempio, +apache lucene ` equivalente a e apache OR (apache AND lucene).

Filtrare i permessi: Esempio (I)

Supponiamo che a ciascun utente di un motore di ricerca si vogliano mostrare come risultati solo i documenti di cui lutente ` autore; e Se i documenti hanno un campo author che elenca tutti gli autori, e authorID ` lidenticativo di un certo utente, allora e una query java verr` espansa nella stringa a java +author:authorID.

Filtrare i permessi: Esempio (II)

Supponiamo che il motore di ricerca abbia a che fare con un insieme di documenti in cui valgano tre gradi di permessi di scrittura, ossia user, editor, e admin (in ordine crescente di importanza):

Lamministratore pu` modicare qualsiasi documento; o Leditor pu` modicare tutti i documenti che gli sono stati o assegnati dalladministrator; Lutente pu` modicare solo i documenti di cui ` autore. o e

Supponiamo di voler mostrare come risultati solo i documenti per cui si ha il permesso di scrittura. Se i documenti hanno dei campi user, editor, e admin, che elencano i rispettivi utenti/editor/amministratori, allora una query java da parte dellutente ID verr` espansa nella stringa a java +(user:ID editor:ID admin:ID), oppure in java AND (user:ID OR editor:ID OR admin:ID).

Interfacciarsi con un database tramite JDBC

JDBC (Java Database Connectivity) ` una API (inclusa nel e pacchetto java.sql del Java Development Kit) che permette ad un client di accedere ad un database SQL; Lo scopo di JDBC ` fornire metodi per interrogare ed e aggiornare un database dallinterno di unapplicazione Java; Il meccanismo di base per lavorare su un database tramite JDBC consiste di due passi:1. Creare una connessione al database sfruttando un driver oerto da JDBC; 2. Eseguire sul database delle istruzioni SQL attraverso la connessione creata.

Le istruzioni che ` possibile eseguire con JDBC sono sia delle e semplici interrogazioni (SELECT), sia delle operazioni di scrittura (CREATE, INSERT, UPDATE, DELETE).

JDBC: Creare una connessione a un database MySQL

Per creare una connessione a un database MySQL, sono sucienti le seguenti istruzioni:1. Class.forName("com.mysql.jdbc.Driver") .newInstance(); 2. Connection connection = DriverManager .getConnection(dbURL, dbUsername, dbPassword);

La prima istruzione chiede allapplicazione di caricare il driver per MySQL; La seconda istruzione sfrutta il driver caricato per creare la connessione al database, utlizzando tre stringhe che specicano lURL, lo username, e la password del database a cui si vuole accedere. La struttura tipica dellURL sar`, ad esempio, come a nella stringa "jdbc:mysql://localhost:3306/dbName", dove jdbc:mysql ` il protocollo di connessione, e localhost:3306 ` lindirizzo e la porta della macchina su cui e si trova il database, e dbName ` il nome del database. e

JDBC: Esempio di interrogazione a un database MySQL

Supponiamo di lavorare con una tabella di nome book, che contiene dei record di libri in cui vengono memorizzati il contenuto del libro (content), come dato di tipo BLOB, e un identicatore numerico del libro (book_ID); Per eseguire uninterrogazione che ottenga il contenuto del libro con ID 01, occorrono le seguenti istruzioni:1. Statement s = connection.createStatement(); 2. statement.executeQuery ("SELECT content FROM book WHERE book_ID=01"); 3. ResultSet rs = s.getResultSet(); 4. rs.first(); 5. Blob blob = rs.getBlob("content"); 6. byte[] bytes = blob.getBytes(1, (int)blob.length()); 7. String content = new String(bytes);