Ricerca full-text
La ricerca full-text è un modo efficace per cercare il testo nel database. Dovresti già avere familiarità con il funzionamento degli indici, ma andiamo oltre le basi.
Un indice funziona come una tabella di ricerca, consentendo al motore di query di trovare rapidamente i record con un determinato valore. Ad esempio, se hai un campo title
nel tuo oggetto, puoi creare un indice su quel campo per rendere più veloce la ricerca di oggetti con un determinato titolo.
Perché la ricerca full-text è utile?
Puoi cercare facilmente il testo usando i filtri. Esistono varie operazioni sulle stringhe, ad esempio .startsWith()
, .contains()
e .matches()
. Il problema con i filtri è che il loro runtime è O(n)
dove n
è il numero di record nella raccolta. Le operazioni sulle stringhe come .matches()
sono particolarmente costose.
Suggerimento
La ricerca full-text è molto più veloce dei filtri, ma gli indici presentano alcune limitazioni. In questa ricetta, esploreremo come aggirare queste limitazioni.
Esempio di base
L'idea è sempre la stessa: invece di indicizzare l'intero testo, indicizziamo le parole nel testo in modo da poterle cercare singolarmente.
Creiamo l'indice full-text più semplice:
class Message {
late int id;
late String content;
@Index()
List<String> get contentWords => content.split(' ');
}
Ora possiamo cercare messaggi con parole specifiche nel contenuto:
final posts = await isar.messages
.where()
.contentWordsAnyEqualTo('hello')
.findAll();
Questa query è super veloce, ma ci sono alcuni problemi:
- Possiamo cercare solo parole intere
- Non consideriamo la punteggiatura
- Non supportiamo altri caratteri di spazio vuoto
Dividere il testo nel modo giusto
Proviamo a migliorare l'esempio precedente. Potremmo provare a sviluppare un'espressione regolare complicata per correggere la divisione delle parole, ma probabilmente sarà lenta e sbagliata per i casi limite.
L'Unicode Annex #29 definisce come dividere correttamente il testo in parole per quasi tutte le lingue. È piuttosto complicato, ma fortunatamente Isar fa il lavoro pesante per noi:
Isar.splitWords('hello world'); // -> ['hello', 'world']
Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?');
// -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right']
Voglio più controllo
Facilissimo! Possiamo modificare il nostro indice anche per supportare la corrispondenza dei prefissi e la corrispondenza senza distinzione tra maiuscole e minuscole:
class Post {
late int id;
late String title;
@Index(type: IndexType.value, caseSensitive: false)
List<String> get titleWords => title.split(' ');
}
Per impostazione predefinita, Isar memorizzerà le parole come valori hash che sono veloci ed efficienti in termini di spazio. Ma gli hash non possono essere usati per la corrispondenza dei prefissi. Usando IndexType.value
, possiamo cambiare l'indice per usare invece le parole direttamente. Ci fornisce la clausola where .titleWordsAnyStartsWith()
:
final posts = await isar.posts
.where()
.titleWordsAnyStartsWith('hel')
.or()
.titleWordsAnyStartsWith('welco')
.or()
.titleWordsAnyStartsWith('howd')
.findAll();
.endsWith()
Ho anche bisogno di Sicuramente! Useremo un trucco per ottenere la corrispondenza .endsWith()
:
class Post {
late int id;
late String title;
@Index(type: IndexType.value, caseSensitive: false)
List<String> get revTitleWords {
return Isar.splitWords(title).map(
(word) => word.reversed).toList()
);
}
}
Non dimenticare di invertire il finale che vuoi cercare:
final posts = await isar.posts
.where()
.revTitleWordsAnyStartsWith('lcome'.reversed)
.findAll();
Algoritmi di derivazione
Sfortunatamente, gli indici non supportano la corrispondenza .contains()
(questo vale anche per altri database). Ma ci sono alcune alternative che vale la pena esplorare. La scelta dipende molto dal tuo utilizzo. Un esempio è l'indicizzazione delle radici delle parole anziché dell'intera parola.
Un algoritmo stemming è un processo di normalizzazione linguistica in cui le forme varianti di una parola sono ridotte a una forma comune:
connection
connections
connective ---> connect
connected
connecting
Gli algoritmi più diffusi sono Algoritmo di stemming di Porter e Algoritmi di stemming di Snowball.
Esistono anche forme più avanzate come lemmatizzazione.
Algoritmi fonetici
Un algoritmo fonetico è un algoritmo per indicizzare le parole in base alla loro pronuncia. In altre parole, ti permette di trovare parole che suonano simili a quelle che stai cercando.
Attenzione
La maggior parte degli algoritmi fonetici supporta solo una singola lingua.
Soundex
Soundex è un algoritmo fonetico per indicizzare i nomi in base al suono, come si pronuncia in inglese. L'obiettivo è che gli omofoni siano codificati nella stessa rappresentazione in modo che possano essere abbinati nonostante piccole differenze nell'ortografia. È un algoritmo semplice e ci sono più versioni migliorate.
Usando questo algoritmo, sia "Robert"
che "Rupert"
restituiscono la stringa "R163"
mentre "Rubin"
restituisce "R150"
. "Ashcraft"
e "Ashcroft"
producono entrambi "A261"
.
Double Metaphone
L'algoritmo di codifica fonetica Double Metaphone è la seconda generazione di questo algoritmo. Apporta diversi miglioramenti di progettazione fondamentali rispetto all'algoritmo Metaphone originale.
Double Metaphone spiega varie irregolarità in inglese di origine slava, germanica, celtica, greca, francese, italiana, spagnola, cinese e di altro tipo.