2009-11-21 11 views
13

È possibile ottimizzare una query di dati principali durante la ricerca di parole corrispondenti in un testo? (Questa domanda riguarda anche la saggezza di SQL personalizzato rispetto a Core Data su un iPhone.)Come ottimizzare la query di dati fondamentali per la ricerca testo completo

Sto lavorando a una nuova app (iPhone) che è uno strumento di riferimento portatile per un database scientifico. L'interfaccia principale è una vista tabella standard ricercabile e voglio una risposta as-you-type mentre l'utente digita le nuove parole. Le corrispondenze di parole devono essere prefissi di parole nel testo. Il testo è composto da 100.000 di parole.

Nel mio prototipo ho codificato direttamente SQL. Ho creato una tabella "parole" separata contenente ogni parola nei campi di testo dell'entità principale. Ho indicizzato le parole e ho eseguito ricerche lungo le linee di

SELECT id, * FROM textTable 
    JOIN (SELECT DISTINCT textTableId FROM words 
     WHERE word BETWEEN 'foo' AND 'fooz') 
    ON id=textTableId 
LIMIT 50 

Questo funziona molto velocemente. Probabilmente l'utilizzo di un IN funzionerebbe altrettanto bene, ovvero

SELECT * FROM textTable 
WHERE id IN (SELECT textTableId FROM words 
       WHERE word BETWEEN 'foo' AND 'fooz') 
LIMIT 50 

Il LIMIT è fondamentale e consente di visualizzare i risultati rapidamente. Notifica all'utente che ce ne sono troppi da visualizzare se viene raggiunto il limite. Questo è kludgy.

Ho passato gli ultimi giorni a riflettere sui vantaggi del passaggio a Core Data, ma mi preoccupo della mancanza di controllo nello schema, dell'indicizzazione e dell'interrogazione per una query importante.

In teoria un NSPredicate di textField MATCHES '.*\bfoo.*' funzionerebbe, ma sono sicuro che sarà lento. Questo tipo di ricerca di testo sembra così comune che mi chiedo quale sia il solito attacco? Vuoi creare un'entità di parole come ho fatto sopra e usare un predicato di "parola BEGINSWITH 'foo'"? Funzionerà così veloce come il mio prototipo? I dati principali creeranno automaticamente gli indici corretti? Non riesco a trovare alcun mezzo esplicito per consigliare lo store persistente sugli indici.

Vedo alcuni vantaggi di Core Data nella mia app per iPhone. Le anomalie e altre considerazioni sulla memoria consentono recuperi di database efficienti per le query tableview senza impostare limiti arbitrari. La gestione del grafo degli oggetti mi consente di attraversare facilmente le entità senza scrivere molto SQL. Le funzionalità di migrazione saranno belle in futuro. D'altra parte, in un ambiente con risorse limitate (iPhone) mi preoccupo che un database generato automaticamente sia gonfio di metadati, relazioni inverse non necessarie, tipi di attributi inefficienti, ecc.

Devo immergermi o procedere con cautela? .

risposta

10

Ho fatto una soluzione alternativa. Penso che sia simile a this post. Ho aggiunto il codice sorgente della fusione al mio progetto Core Data, quindi ho creato una classe di ricerca full-text che non era una sottoclasse di oggetti gestiti. Nella classe FTS I #import "sqlite3.h" (il file di origine) invece del framework sqlite. La classe FTS salva in un diverso file .sqlite rispetto all'archivio persistente di Core Data.

Quando importo i miei dati, l'oggetto Dati di base memorizza il rowid dell'oggetto FTS correlato come attributo intero. Ho un dataset statico, quindi non mi preoccupo dell'integrità referenziale, ma il codice per mantenere l'integrità dovrebbe essere banale.

Per eseguire FTS, I MATCH interrogare la classe FTS, restituendo un set di rowid. Nella mia classe di oggetti gestiti, eseguo una query per gli oggetti corrispondenti con [NSPredicate predicateWithFormat:@"rowid IN %@", rowids]. Evito di attraversare qualsiasi relazione molti-a-molti in questo modo.

Il miglioramento delle prestazioni è drammatico. Il mio set di dati è 142287 righe, che comprendono 194 MB (dati principali) e 92 MB (FTS con stopword rimossi). A seconda della frequenza del termine di ricerca, le mie ricerche sono passate da alcuni secondi a 0,1 secondi per termini non frequenti (< 100 hit) e 0,2 secondi per termini frequenti (> 2000 hit).

Sono sicuro che ci sono una miriade di problemi con il mio approccio (code bloat, possibili conflitti nello spazio dei nomi, perdita di alcune funzionalità di Core Data), ma sembra funzionare.

2

Dive in

Ecco un modo per andare a questo proposito:

  1. Metti il ​​tuo record in un archivio permanente Core Data
  2. Usa NSFetchedResultsController per gestire un set di risultati sui vostri Word entità (Core Dati equivalenti con la tabella "parole" SQL)
  3. Utilizzare UISearchDisplayController per applicare uno NSPredicate sul set di risultati in tempo reale

Una volta impostato un risultato tramite NSFetchedResultsController, è abbastanza semplice applicare un predicato. Nella mia esperienza sarà anche reattivo.Per esempio:

if ([self.searchBar.text length]) { 
    _predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"(word contains[cd] '%@')", self.searchBar.text]]; 
    [self.fetchedResultsController.fetchRequest setPredicate:_predicate]; 
} 

NSError *error; 
if (![self.fetchedResultsController performFetch:&error]) { 
    // handle error... 
} 
NSLog(@"filtered results: %@", [self.fetchedResultsController fetchedObjects]); 

filtrerà il set di risultati [self.fetchedResultsController fetchedObjects] al volo, facendo una ricerca case-insensitive su word.

+0

Grazie per la risposta. Sto solo scrivendo lo strumento da riga di comando per caricare i dati sqlite iniziali in un db compatibile con xcdatamodel. Lavoro sostanziale coinvolto. Riferirò sulla mia esperienza. –

+0

Per seguire il tuo esempio, penso che il problema è che una richiesta di recupero non si trova nell'entità di Word, ma nell'entità textTable. (Supponiamo che textTable contenga messaggi di posta elettronica e Word contenga tutte le parole in tutti i campi di posta elettronica.) Penso che questo complichi significativamente la faccenda perché fetchResultsController deve contenere entità textTable filtrate tramite un predicato - e tale predicato ANY o SUBQUERY è lento. Forse c'è un modo per farlo nella direzione "opposta": avviando w/Word corrisponde, seguendo la relazione inversa, e univoca textTable. Hmmm. –

+0

Se la prima parte del predicato riduce lo spazio di ricerca il più possibile, il resto del predicato verrà eseguito più rapidamente, in generale, con meno spazio da cercare all'interno. Dai un'occhiata alla sezione performance della guida Core Data qui: http://developer.apple.com/mac/library/documentation/cocoa/conceptual/CoreData/Articles/cdPerformance.html#//apple_ref/doc/uid/ TP40003468 –

3

Per dare seguito a questa domanda, ho trovato che l'interrogazione è lenta con l'uso di Core Data. Mi sono grattato la testa per ore.

Come nell'esempio SQL nella mia domanda, ci sono due entità: textTable e parole in cui le parole contengono ogni parola, sono indicizzate e c'è una relazione molti-a-molti tra textTable e words. Ho popolato il database con solo 4000 parole e 360 ​​oggetti textTable. Supponiamo che il rapporto textTable alle parole oggetto è chiamato SearchWords, quindi posso usare un predicato sull'entità textTable che assomiglia

predicate = [NSPredicate predicateWithFormat:@"ANY searchWords.word BEGINSWITH %@", query]; 

(posso aggiungere congiunzioni di questo predicato per più termini di ricerca.)

Su iPhone questa query richiede più secondi. La risposta per il mio codice SQL scritto a mano con un set di test più ampio è stata istantanea.

Ma questa non è nemmeno la fine. Esistono limitazioni a NSPredicate che rendono le query piuttosto semplici lente e complesse. Ad esempio, immagina nell'esempio sopra che desideri filtrare utilizzando un pulsante di ambito. Supponiamo che l'entità parole contenga tutte le parole in tutti i campi di testo, ma l'ambito lo limiterà a parole provenienti da campi specifici. Pertanto, le parole potrebbero avere un attributo "fonte" (ad esempio intestazione e corpo del messaggio di posta elettronica).

Naturalmente, un intero testo ignorerebbe l'attributo di origine, come nell'esempio sopra, ma una query filtrata limiterebbe la ricerca a un particolare valore di origine. Questo cambiamento apparentemente semplice richiede una SUBQUERY. Ad esempio, questa non funziona:

ANY searchWords.word BEGINSWITH "foo" AND ANY searchWords.source = 3 

perché le entità che sono vere per le due espressioni possono essere diverse. Invece, si deve fare qualcosa di simile:

SUBQUERY(searchWords, $x, $x.word BEGINSWITH "foo" AND $x.source = 3)[email protected] > 0 

Ho scoperto che queste subquery sono, forse non a caso, più lento di predicati con "ANY".

A questo punto sono molto curioso del modo in cui i programmatori Cocoa utilizzano in modo efficiente i dati principali per la ricerca a testo integrale perché sono scoraggiato sia dalla velocità di valutazione dei predicati sia dall'espressibilità di NSPredicates. Ho incontrato un muro.

+1

Considera di dare un'occhiata alla sezione delle prestazioni qui: http://developer.apple.com/mac/library/documentation/cocoa/conceptual/CoreData/Articles/cdPerformance.html#//apple_ref/doc/uid/TP40003468 –

+0

Grazie per questo link. Da lì ho scoperto che l'argomento eseguibile "-com.apple.CoreData.SQLDebug 1" invierà il debug sqlite a stderr. Da quella discarica ho visto la domanda. Non c'era nulla di veramente sbagliato nella query, ma poiché la parola <=> textTable è una relazione molti-a-molti c'è una tabella delle relazioni da unire. Pertanto, la query deve essere associata a 3 tabelle. Quando ho rimosso l'inversa ora la query viene eseguita molto più velocemente sull'hardware dell'iPhone! Purtroppo, il nuovo schema ha la chiave esterna nella tabella di Word, quindi la parola stessa e i metadati vengono ripetuti per ogni occorrenza. Spazio sprecato –

+0

È possibile aumentare la velocità, ma Apple consiglia di mantenere relazioni inverse per mantenere l'integrità dei dati. "In genere è necessario modellare le relazioni in entrambe le direzioni e specificare le relazioni inverse in modo appropriato: i dati di base utilizzano queste informazioni per garantire la coerenza del grafico degli oggetti se viene apportata una modifica (vedere" Manipolazione delle relazioni e integrità del grafico degli oggetti ")." Dai un'occhiata qui per maggiori informazioni: http://developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/CoreData/Articles/cdRelationships.html –

2

Dopo aver lottato con questo stesso problema, mi sono imbattuto in una serie di post in cui l'autore ha avuto lo stesso problema e si avvicinò con this solution. Segnala un miglioramento da 6-7 secondi di tempo di ricerca a tra 0,13 e 0,05 secondi.

Il suo set di dati per FTS era di 79 documenti (dimensione del file 175k, 3600 token discreti, 10000 riferimenti). Non ho ancora provato la sua soluzione, ma ho pensato di pubblicare AL PIÙ PRESTO. Vedi anche Part 2 dei suoi post per la sua documentazione del problema e Part 1 per la sua documentazione del set di dati.

+0

Il problema che ho con questa soluzione è che la query e la parola chiave devono essere una corrispondenza esatta. Per i risultati in tempo reale, si desidera che qualsiasi prefisso di parola chiave corrisponda alla query.In tal caso non è possibile utilizzare l'oggetto al posto della stringa nel predicato. –

+0

Provato a implementarlo da solo e senza alcun miglioramento, probabilmente perché stavo usando contiene [cd]. Mi sono arreso e ho iniziato con sqlite3 fts. Peter, grazie per i link extra. Ero limitato a uno solo. – jluckyiv

Problemi correlati