Algoritmo per trovare i primi 10 termini di ricerca


115

Attualmente mi sto preparando per un'intervista e mi ha ricordato una domanda che mi è stata posta una volta in una precedente intervista che era qualcosa del genere:

"Ti è stato chiesto di progettare un software per visualizzare continuamente i primi 10 termini di ricerca su Google. Hai accesso a un feed che fornisce un flusso infinito in tempo reale di termini di ricerca attualmente ricercati su Google. Descrivi quale algoritmo e quali strutture di dati utilizzeresti per implementarlo. Devi progettare due varianti:

(i) Visualizza i primi 10 termini di ricerca di tutti i tempi (ovvero da quando hai iniziato a leggere il feed).

(ii) Visualizza solo i primi 10 termini di ricerca dell'ultimo mese, aggiornati ogni ora.

Puoi usare un'approssimazione per ottenere la lista dei primi 10, ma devi giustificare le tue scelte. "
Ho bombardato in questa intervista e ancora non ho davvero idea di come implementarlo.

La prima parte richiede le 10 voci più frequenti in una sotto-sequenza in continua crescita di un elenco infinito. Ho esaminato gli algoritmi di selezione, ma non sono riuscito a trovare alcuna versione online per risolvere questo problema.

La seconda parte utilizza un elenco finito, ma a causa della grande quantità di dati elaborati, non è possibile archiviare in memoria l'intero mese di termini di ricerca e calcolare un istogramma ogni ora.

Il problema è reso più difficile dal fatto che l'elenco dei primi 10 viene aggiornato continuamente, quindi in qualche modo è necessario calcolare i primi 10 su una finestra scorrevole.

Qualche idea?


11
@BlueRaja - Non è una domanda stupida per l'intervista, è una cattiva interpretazione da parte dell'OP. Non sta chiedendo gli elementi più frequenti in una lista infinita, sta chiedendo gli elementi più frequenti di una sottosequenza finita di una lista infinita. Per continuare la tua analogia,what is the most frequent item in the subsequence [2; 2; 3; 3; 3; 4; 4; 4; 4; 5; 5] of your sequence?
IVlad

3
@BlueRaja - È certamente una domanda difficile, ma non vedo perché sia ​​stupida: sembra rappresentativa di un problema abbastanza tipico che devono affrontare le aziende con enormi set di dati. @IVlad - Risolto come da tuo suggerimento, pessima formulazione da parte mia!
del

Risposte:


47

Bene, sembra un sacco di dati, con un costo forse proibitivo per memorizzare tutte le frequenze. Quando la quantità di dati è così grande che non possiamo sperare di archiviarli tutti, entriamo nel dominio degli algoritmi del flusso di dati .

Libro utile in quest'area: Muthukrishnan - "Data Streams: Algorithms and Applications"

Riferimento strettamente correlato al problema in questione che ho scelto da quanto sopra: Manku, Motwani - "Conteggi di frequenza approssimativi sui flussi di dati" [pdf]

A proposito, Motwani, di Stanford, (modifica) è stato un autore dell'importantissimo libro "Randomized Algorithms" . L'undicesimo capitolo di questo libro tratta questo problema . Modifica: scusa, riferimento errato, quel particolare capitolo tratta un problema diverso. Dopo aver controllato, consiglio invece la sezione 5.1.2 del libro di Muthukrishnan , disponibile online.

Eh, bella domanda per l'intervista.


2
+1 Cose molto interessanti, dovrebbe esserci un modo sui siti per taggare le cose "da leggere". Grazie per la condivisione.
Ramadheer Singh

@Gollum: ho una cartella da leggere nei miei segnalibri; potresti semplicemente farlo. So che questi collegamenti vengono aggiunti al mio :)
Cam

+1. Gli algoritmi di streaming sono esattamente l'argomento qui, e il libro di Muthu (l'unico libro scritto finora, AFAIK) è fantastico.
ShreevatsaR

1
+1. Correlato: en.wikipedia.org/wiki/Online_algorithm . btw, Motwani è morto di recente, quindi forse era un autore più preciso.

Molto strano. Lo conoscevo dal libro, ma sicuramente doveva essere più famoso per questo: "Motwani è stato uno dei coautori (con Larry Page, Sergey Brin e Terry Winograd) di un influente articolo iniziale sull'algoritmo del PageRank, la base per le tecniche di ricerca di Google. "( en.wikipedia.org/wiki/Rajeev_Motwani )
Dimitris Andreou

55

Panoramica sulla stima della frequenza

Esistono alcuni algoritmi ben noti che possono fornire stime di frequenza per tale flusso utilizzando una quantità fissa di memoria. One is Frequent, di Misra e Gries (1982). Da un elenco di n elementi, trova tutti gli elementi che ricorrono più di n / k volte, utilizzando i contatori k - 1 . Questa è una generalizzazione di Boyer e Moore di Maggioranza algoritmo (Fischer-Salzberg, 1982), dove k è 2. Manku e di Motwani LossyCounting (2002) e di Metwally salvaspazio (2005) algoritmi hanno requisiti di spazio simili, ma in grado di fornire stime più accurate sotto certi condizioni.

La cosa importante da ricordare è che questi algoritmi possono fornire solo stime di frequenza. In particolare, la stima di Misra-Gries può sottovalutare la frequenza effettiva di (n / k) elementi.

Supponiamo di avere un algoritmo in grado di identificare positivamente un elemento solo se si verifica più del 50% delle volte. Alimenta questo algoritmo con un flusso di N elementi distinti, quindi aggiungi altre N - 1 copie di un elemento, x , per un totale di 2N - 1 elementi. Se l'algoritmo ti dice che x supera il 50% del totale, deve essere stato nel primo flusso; in caso contrario, x non era nel flusso iniziale. Affinché l'algoritmo possa effettuare questa determinazione, deve memorizzare il flusso iniziale (o un riepilogo proporzionale alla sua lunghezza)! Quindi, possiamo provare a noi stessi che lo spazio richiesto da un tale algoritmo "esatto" sarebbe Ω ( N ).

Invece, questi algoritmi di frequenza qui descritti forniscono una stima, identificando qualsiasi elemento che supera la soglia, insieme ad alcuni elementi che scendono al di sotto di essa con un certo margine. Ad esempio l' algoritmo Maggioranza , utilizzando un unico contatore, darà sempre un risultato; se un elemento supera il 50% del flusso, verrà trovato. Ma potrebbe anche darti un elemento che si verifica una sola volta. Non lo sapresti senza fare un secondo passaggio sui dati (usando, ancora una volta, un singolo contatore, ma cercando solo quell'elemento).

L'algoritmo frequente

Ecco una semplice descrizione dell'algoritmo Frequent di Misra-Gries . Demaine (2002) e altri hanno ottimizzato l'algoritmo, ma questo ti dà il succo.

Specificare la frazione di soglia, 1 / k ; qualsiasi elemento che si verifica più di n / k volte verrà trovato. Crea una mappa vuota (come un albero rosso-nero); le chiavi saranno i termini di ricerca e i valori saranno un contatore per quel termine.

  1. Guarda ogni elemento nello stream.
  2. Se il termine esiste nella mappa, incrementa il contatore associato.
  3. Altrimenti, se la mappa è inferiore a k - 1 voci, aggiungi il termine alla mappa con un contatore di uno.
  4. Tuttavia, se la mappa ha già k - 1 voci, diminuire il contatore in ogni voce. Se un qualsiasi contatore raggiunge lo zero durante questo processo, rimuoverlo dalla mappa.

Tieni presente che puoi elaborare una quantità infinita di dati con una quantità fissa di archiviazione (solo la mappa a dimensione fissa). La quantità di archiviazione richiesta dipende solo dalla soglia di interesse e la dimensione del flusso non è importante.

Conteggio delle ricerche

In questo contesto, forse bufferizzi un'ora di ricerche ed esegui questo processo sui dati di quell'ora. Se puoi eseguire un secondo passaggio nel registro di ricerca di questa ora, puoi ottenere un conteggio esatto delle occorrenze dei primi "candidati" identificati nel primo passaggio. O forse va bene fare un singolo passaggio e segnalare tutti i candidati, sapendo che qualsiasi elemento che dovrebbe essere lì è incluso e qualsiasi extra è solo rumore che scomparirà nell'ora successiva.

Tutti i candidati che superano davvero la soglia di interesse vengono archiviati come riepilogo. Conserva un mese di questi riepiloghi, buttando via il più vecchio ogni ora, e avrai una buona approssimazione dei termini di ricerca più comuni.


Credo che questa soluzione possa fungere da filtro, riducendo il numero di termini di ricerca di tuo interesse. Se un termine compare nella mappa, inizia a tracciare le sue statistiche effettive, anche se cade fuori dalla mappa. È quindi possibile saltare il secondo passaggio sui dati e produrre una top 10 ordinata dalle statistiche limitate raccolte.
Dolph

Mi piace il modo elegante di potare i termini meno cercati dall'albero diminuendo i contatori. Ma una volta che la mappa è "piena", non sarà necessario un passaggio di decremento per ogni nuovo termine di ricerca che arriva? E una volta che questo inizia ad accadere, ciò non comporterà la rimozione rapida dei termini di ricerca più recenti dalla mappa prima che abbiano la possibilità che i loro contatori aumentino sufficientemente?
del

1
@del - Tieni presente che questo algoritmo serve per individuare i termini che superano una frequenza di soglia specificata, non necessariamente per trovare i termini più comuni. Se i termini più comuni cadono al di sotto della soglia specificata, generalmente non verranno trovati. La tua preoccupazione per la rimozione dei termini più recenti "troppo rapidamente" potrebbe essere associata a questo caso. Un modo per vedere questo è che ci sono dei veri "segnali" in popolarità, che risaltano notevolmente dal "rumore". Ma a volte non ci sono segnali da trovare, solo ricerca statica casuale.
erickson

@erickson - Giusto - quello che sto arrivando è che il presupposto con questo algoritmo è che le prime 10 parole siano distribuite uniformemente nella finestra di misurazione. Ma fintanto che si mantiene la finestra di misurazione sufficientemente piccola (ad es. 1 ora), questo sarebbe probabilmente un presupposto valido.
del

1
@erickson, sebbene una distribuzione uniforme non sia un requisito, mi chiedo come funzionerebbe in una distribuzione più realistica (power-law, Zipf). Supponiamo di avere N parole distinte e manteniamo l'albero rosso-nero di capacità K, sperando che finisca con i termini K più frequenti. Se la frequenza cumulativa dei termini di (N - K) parole è maggiore della frequenza cumulativa delle K parole più frequenti, è garantito che l'albero alla fine contenga spazzatura. Sei d'accordo?
Dimitris Andreou

19

Questo è uno dei progetti di ricerca che sto attualmente attraversando. Il requisito è quasi esattamente come il tuo e abbiamo sviluppato dei simpatici algoritmi per risolvere il problema.

L'input

L'input è un flusso infinito di parole o frasi inglesi (le chiamiamo tokens).

Il risultato

  1. Emetti i migliori N token che abbiamo visto finora (da tutti i token che abbiamo visto!)
  2. Produci i primi N token in una finestra storica, ad esempio l'ultimo giorno o la settimana scorsa.

Un'applicazione di questa ricerca è trovare l'argomento caldo o le tendenze dell'argomento su Twitter o Facebook. Abbiamo un crawler che esegue la scansione del sito Web, che genera un flusso di parole, che alimenterà il sistema. Il sistema quindi emetterà le parole o le frasi di massima frequenza sia a livello generale che storico. Immagina che nelle ultime due settimane la frase "Coppa del mondo" sia apparsa molte volte su Twitter. Così fa "Paul the octopus". :)

String in numeri interi

Il sistema ha un ID intero per ogni parola. Sebbene ci siano quasi infinite parole possibili su Internet, ma dopo aver accumulato un ampio insieme di parole, la possibilità di trovare nuove parole diventa sempre più bassa. Abbiamo già trovato 4 milioni di parole diverse e assegnato un ID univoco a ciascuna. L'intero set di dati può essere caricato in memoria come tabella hash, consumando circa 300 MB di memoria. (Abbiamo implementato la nostra tabella hash. L'implementazione di Java richiede un enorme sovraccarico di memoria)

Ogni frase può quindi essere identificata come una matrice di numeri interi.

Questo è importante, perché l'ordinamento e il confronto sugli interi è molto più veloce che sulle stringhe.

Dati di archivio

Il sistema conserva i dati di archivio per ogni token. Fondamentalmente sono coppie di (Token, Frequency). Tuttavia, la tabella che memorizza i dati sarebbe così enorme da dover partizionare fisicamente la tabella. Una volta che lo schema di partizione è basato su ngram del token. Se il token è una singola parola, è 1 grammo. Se il token è una frase di due parole, è 2gram. E questo continua. All'incirca a 4 grammi abbiamo 1 miliardo di record, con una dimensione della tabella di circa 60 GB.

Elaborazione di flussi in entrata

Il sistema assorbirà le frasi in arrivo fino a quando la memoria non sarà completamente utilizzata (Sì, abbiamo bisogno di un MemoryManager). Dopo aver preso N frasi e averlo memorizzato, il sistema si ferma e inizia a tokenizzare ogni frase in parole e frasi. Ogni token (parola o frase) viene conteggiato.

Per i gettoni molto frequenti, vengono sempre mantenuti in memoria. Per i token meno frequenti, vengono ordinati in base agli ID (ricorda che traduciamo la stringa in un array di numeri interi) e serializzati in un file su disco.

(Tuttavia, per il tuo problema, dal momento che stai contando solo parole, puoi mettere tutta la mappa della frequenza delle parole solo in memoria. Una struttura di dati progettata con cura consumerebbe solo 300 MB di memoria per 4 milioni di parole diverse. Qualche suggerimento: usa caratteri ASCII per rappresentano stringhe), e questo è molto accettabile.

Nel frattempo, ci sarà un altro processo che si attiva una volta che trova un file su disco generato dal sistema, quindi inizia a unirlo. Poiché il file su disco è ordinato, l'unione richiederebbe un processo simile come l'ordinamento di unione. Anche in questo caso è necessario curare alcuni progetti, poiché vogliamo evitare troppe ricerche casuali su disco. L'idea è di evitare la lettura (processo di unione) / scrittura (output di sistema) allo stesso tempo e lasciare che il processo di unione legga da un disco mentre scrive su un disco diverso. È simile all'implementazione di un blocco.

Fine del giorno

Alla fine della giornata, il sistema avrà molti token frequenti con la frequenza archiviata in memoria e molti altri token meno frequenti archiviati in diversi file su disco (e ogni file è ordinato).

Il sistema scarica la mappa in memoria in un file su disco (ordinalo). Ora, il problema diventa l'unione di un insieme di file su disco ordinati. Utilizzando un processo simile, otterremmo un file su disco ordinato alla fine.

Quindi, l'operazione finale è unire il file del disco ordinato nel database di archivio. Dipende dalla dimensione del database di archivio, l'algoritmo funziona come di seguito se è abbastanza grande:

   for each record in sorted disk file
        update archive database by increasing frequency
        if rowcount == 0 then put the record into a list
   end for

   for each record in the list of having rowcount == 0
        insert into archive database
   end for

L'intuizione è che dopo qualche tempo il numero di inserimenti diventerà sempre più piccolo. Sempre più operazioni saranno solo sull'aggiornamento. E questo aggiornamento non sarà penalizzato da index.

Spero che questa intera spiegazione possa aiutare. :)


Non lo capisco. Che tipo di ordinamento o confronto significativo si potrebbe fare negli ID interi delle parole? I numeri non sono arbitrari?
Dimitris Andreou

Inoltre, il conteggio delle frequenze delle parole è il primo esempio nel documento MapReduce di Google ( labs.google.com/papers/mapreduce.html ), risolvendolo in modo scalabile in una manciata di righe. Potresti persino spostare i tuoi dati su google app angine e fare un simile MapReduce ( code.google.com/p/appengine-mapreduce )
Dimitris Andreou

@ Dimitris Andreou: l'ordinamento su numeri interi sarebbe più veloce sulle stringhe. Questo perché il confronto di due numeri interi è più veloce del confronto di due stringhe.
SiLent SoNG

@Dimitris Andreou: il mapreduce di Google è un bel approccio distribuito per risolvere questo problema. Ah! Grazie per aver fornito i link. Sì, sarebbe bene per noi ordinare utilizzando più macchine. Bel approccio.
SiLent SoNG

@ Dimitris Andreou: Finora ho preso in considerazione solo l'approccio di smistamento macchina singola. Che bella idea ordinare in distribuzione.
SiLent SoNG

4

Potresti usare una tabella hash combinata con un albero di ricerca binario . Implementa un <search term, count>dizionario che ti dice quante volte ogni termine di ricerca è stato cercato.

Ovviamente l'iterazione l'intera tabella hash ogni ora per ottenere la top 10 è molto male. Ma questo è Google di cui stiamo parlando, quindi puoi presumere che i primi dieci riceveranno tutti, diciamo oltre 10.000 visite (probabilmente è un numero molto più grande). Pertanto, ogni volta che il conteggio di un termine di ricerca supera 10.000, inseriscilo nel BST. Quindi ogni ora, devi solo ottenere i primi 10 dal BST, che dovrebbe contenere relativamente poche voci.

Questo risolve il problema della top-10 di tutti i tempi.


La parte davvero complicata è gestire un termine che prende il posto di un altro nel rapporto mensile (ad esempio, "overflow dello stack" potrebbe avere 50.000 visite negli ultimi due mesi, ma solo 10.000 il mese scorso, mentre "amazon" potrebbe averne 40 000 per gli ultimi due mesi ma 30 000 per il mese scorso. Vuoi che "amazon" venga prima di "overflow dello stack" nel tuo rapporto mensile). Per fare ciò, memorizzerei, per tutti i principali termini di ricerca (oltre 10.000 ricerche di tutti i tempi), un elenco di 30 giorni che ti dice quante volte quel termine è stato cercato ogni giorno. L'elenco funzionerebbe come una coda FIFO: rimuovi il primo giorno e ne inserisci uno nuovo ogni giorno (o ogni ora, ma poi potresti dover memorizzare più informazioni, il che significa più memoria / spazio. Se la memoria non è un problema, fallo altrimenti scegli quella "approssimazione"

Sembra un buon inizio. Puoi quindi preoccuparti di eliminare i termini che hanno> 10.000 hit ma non ne ho avuti molti da molto tempo e cose del genere.


3

caso i)

Mantieni una tabella hash per tutti i termini di ricerca, oltre a un elenco ordinato dei primi dieci separato dalla tabella hash. Ogni volta che viene eseguita una ricerca, incrementa l'elemento appropriato nella tabella hash e controlla se quell'elemento dovrebbe ora essere scambiato con il decimo elemento nell'elenco dei primi dieci.

Ricerca O (1) per l'elenco dei primi dieci e inserimento massimo di O (log (n)) nella tabella hash (supponendo che le collisioni siano gestite da un albero binario autobilanciato).

caso ii) Invece di mantenere un'enorme tabella hash e un piccolo elenco, manteniamo una tabella hash e un elenco ordinato di tutti gli elementi. Ogni volta che viene eseguita una ricerca, quel termine viene incrementato nella tabella hash e nell'elenco ordinato il termine può essere controllato per vedere se deve cambiare con il termine dopo di esso. Un albero binario autobilanciato potrebbe funzionare bene per questo, poiché dobbiamo anche essere in grado di interrogarlo rapidamente (ne parleremo più avanti).

Inoltre manteniamo anche un elenco di "ore" sotto forma di un elenco FIFO (coda). Ogni elemento "ora" conterrebbe un elenco di tutte le ricerche effettuate in quella particolare ora. Quindi, ad esempio, il nostro elenco di ore potrebbe essere simile a questo:

Time: 0 hours
      -Search Terms:
          -free stuff: 56
          -funny pics: 321
          -stackoverflow: 1234
Time: 1 hour
      -Search Terms:
          -ebay: 12
          -funny pics: 1
          -stackoverflow: 522
          -BP sucks: 92

Quindi, ogni ora: se l'elenco ha una durata di almeno 720 ore (che è il numero di ore in 30 giorni), guarda il primo elemento nell'elenco e, per ogni termine di ricerca, decrementa quell'elemento nella tabella hash della quantità appropriata . Successivamente, elimina il primo elemento dell'ora dall'elenco.

Quindi diciamo che siamo all'ora 721 e siamo pronti a guardare la prima ora nella nostra lista (sopra). Decrementeremmo le cose gratuite di 56 nella tabella hash, le foto divertenti di 321, ecc., E quindi rimuoveremmo completamente l'ora 0 dall'elenco poiché non avremo mai bisogno di guardarla di nuovo.

Il motivo per cui manteniamo un elenco ordinato di tutti i termini che consente query rapide è perché ogni ora dopo aver esaminato i termini di ricerca di 720 ore fa, dobbiamo assicurarci che l'elenco dei primi dieci rimanga ordinato. Quindi, mentre decrementiamo "materiale gratuito" di 56 nella tabella hash, ad esempio, verifichiamo dove si trova ora nell'elenco. Poiché è un albero binario autobilanciato, tutto ciò può essere realizzato bene in tempo O (log (n)).


Modifica: sacrificare la precisione per lo spazio ...

Potrebbe essere utile implementare anche una grande lista nella prima, come nella seconda. Quindi potremmo applicare la seguente ottimizzazione dello spazio su entrambi i casi: eseguire un cron job per rimuovere tutti gli elementi tranne i primi x nell'elenco. Ciò manterrebbe il requisito di spazio basso (e di conseguenza renderebbe più veloci le query nell'elenco). Ovviamente, produrrebbe un risultato approssimativo, ma questo è consentito. x potrebbe essere calcolato prima di distribuire l'applicazione in base alla memoria disponibile e regolato dinamicamente se diventa disponibile più memoria.


2

Pensiero approssimativo ...

Per i primi 10 di tutti i tempi

  • Utilizzo di una raccolta hash in cui è memorizzato un conteggio per ogni termine (disinfetta i termini, ecc.)
  • Un array ordinato che contiene i primi 10 in corso, un termine / conteggio aggiunto a questo array ogni volta che il conteggio di un termine diventa uguale o maggiore del conteggio più piccolo dell'array

Per i primi 10 mensili aggiornati ogni ora:

  • Utilizzando un array indicizzato sul numero di ore trascorse dall'inizio modulo 744 (il numero di ore durante un mese), le voci di array consistono nella raccolta di hash in cui viene memorizzato un conteggio per ogni termine incontrato durante questo intervallo di ore. Una voce viene azzerata ogni volta che cambia il contatore della fascia oraria
  • le statistiche nell'array indicizzate su fascia oraria devono essere raccolte ogni volta che il contatore di fascia oraria corrente cambia (una volta al massimo), copiando e appiattendo il contenuto di questa matrice indicizzata su fasce orarie

Errr ... ha senso? Non ci ho pensato come avrei fatto nella vita reale

Ah sì, dimenticavo di menzionare, la "copia / appiattimento" oraria richiesta per le statistiche mensili può effettivamente riutilizzare lo stesso codice usato per i primi 10 di tutti i tempi, un bell'effetto collaterale.


2

Soluzione esatta

Innanzitutto, una soluzione che garantisce risultati corretti, ma richiede molta memoria (una grande mappa).

Variante "di tutti i tempi"

Mantieni una mappa hash con le query come chiavi e i loro conteggi come valori. Inoltre, tieni un elenco delle 10 query più frequenti finora e il conteggio del decimo conteggio più frequente (una soglia).

Aggiorna costantemente la mappa durante la lettura del flusso di query. Ogni volta che un conteggio supera la soglia corrente, procedi come segue: rimuovi la decima query dall'elenco "Top 10", sostituiscila con la query che hai appena aggiornato e aggiorna anche la soglia.

Variante "Ultimo mese"

Mantieni lo stesso elenco "Top 10" e aggiornalo come sopra. Inoltre, mantieni una mappa simile, ma questa volta memorizza i vettori di 30 * 24 = 720 conteggi (uno per ogni ora) come valori. Ogni ora eseguire le seguenti operazioni per ogni chiave: rimuovere il contatore più vecchio dal vettore aggiungerne uno nuovo (inizializzato a 0) alla fine. Rimuovere la chiave dalla mappa se il vettore è tutto zero. Inoltre, ogni ora devi calcolare da zero la lista "Top 10".

Nota: Sì, questa volta stiamo memorizzando 720 interi invece di uno, ma ci sono molte meno chiavi (la variante di tutti i tempi ha una coda molto lunga).

approssimazioni

Queste approssimazioni non garantiscono la soluzione corretta, ma consumano meno memoria.

  1. Elabora ogni query N-esima, saltando il resto.
  2. (Solo per la variante di tutti i tempi) Mantieni al massimo M coppie chiave-valore nella mappa (M dovrebbe essere grande quanto puoi permetterti). È una specie di cache LRU: ogni volta che leggi una query che non è nella mappa, rimuovi la query utilizzata meno di recente con conteggio 1 e sostituiscila con la query attualmente elaborata.

Mi piace l'approccio probabilistico in approssimazione 1. Ma usando l'approssimazione 2 (cache LRU), cosa succede se termini che inizialmente non erano molto popolari diventano popolari in seguito? Non verrebbero scartati ogni volta che vengono aggiunti, poiché il loro conteggio sarebbe molto basso?
del

@del Hai ragione, la seconda approssimazione funzionerà solo per determinati flussi di query. È meno affidabile, ma allo stesso tempo richiede meno risorse. Nota: puoi anche combinare entrambe le approssimazioni.
Bolo

2

Primi 10 termini di ricerca per il mese scorso

L'utilizzo di una struttura di indicizzazione / dati efficiente in termini di memoria, come i tentativi strettamente impacchettati (dalle voci di Wikipedia sui tentativi ) definisce approssimativamente una relazione tra i requisiti di memoria e il numero n di termini.

Nel caso in cui sia disponibile la memoria richiesta ( presupposto 1 ), è possibile mantenere una statistica mensile esatta e aggregarla ogni mese in una statistica di tutti i tempi.

C'è anche un presupposto che interpreta l '"ultimo mese" come finestra fissa. Ma anche se la finestra mensile è scorrevole, la procedura sopra mostra il principio (lo scorrimento può essere approssimato con finestre fisse di dimensioni date).

Questo mi ricorda il database round-robin con l'eccezione che alcune statistiche sono calcolate su "tutto il tempo" (nel senso che non tutti i dati vengono conservati; rrd consolida i periodi di tempo ignorando i dettagli facendo la media, sommando o scegliendo i valori max / min, in un determinato compito il dettaglio che si perde sono le informazioni sugli elementi a bassa frequenza, che possono introdurre errori).

Assunzione 1

Se non possiamo mantenere statistiche perfette per l'intero mese, allora dovremmo essere in grado di trovare un certo periodo P per il quale dovremmo essere in grado di mantenere statistiche perfette. Ad esempio, supponendo di avere statistiche perfette su un periodo di tempo P, che va nel mese n volte.
Statistiche perfette definiscono la funzione f(search_term) -> search_term_occurance.

Se riusciamo a mantenere nin memoria tutte le tabelle delle statistiche perfette, le statistiche mensili scorrevoli possono essere calcolate in questo modo:

  • aggiungere statistiche per il periodo più recente
  • rimuovere le statistiche per il periodo più vecchio (quindi dobbiamo mantenere ntabelle delle statistiche perfette)

Tuttavia, se manteniamo solo i primi 10 a livello aggregato (mensile), saremo in grado di scartare molti dati dalle statistiche complete del periodo fissato. Ciò fornisce già una procedura di lavoro che ha fissato (assumendo il limite superiore sulla tabella delle statistiche perfette per il periodo P) i requisiti di memoria.

Il problema con la procedura sopra è che se conserviamo le informazioni solo sui primi 10 termini per una finestra scorrevole (in modo simile per sempre), le statistiche saranno corrette per i termini di ricerca che raggiungono il picco in un periodo, ma potrebbero non vedere il statistiche per i termini di ricerca che si diffondono costantemente nel tempo.

Questo può essere compensato mantenendo le informazioni su più dei primi 10 termini, ad esempio i primi 100 termini, sperando che i primi 10 siano corretti.

Penso che un'ulteriore analisi potrebbe mettere in relazione il numero minimo di occorrenze necessarie affinché una voce diventi parte delle statistiche (che è correlata all'errore massimo).

(Nel decidere quali voci dovrebbero entrare a far parte delle statistiche si potrebbe anche monitorare e tracciare le tendenze; ​​per esempio se un'estrapolazione lineare delle occorrenze in ogni periodo P per ogni termine ti dice che il termine diventerà significativo in un mese o due tu potrebbe già iniziare a tracciarlo. Un principio simile si applica per la rimozione del termine di ricerca dal pool tracciato.)

Il caso peggiore per quanto sopra è quando si hanno molti termini quasi ugualmente frequenti e cambiano continuamente (ad esempio se si monitorano solo 100 termini, quindi se i primi 150 termini si verificano con la stessa frequenza, ma i primi 50 sono più spesso nel primo mese e per timore che spesso qualche tempo dopo le statistiche non sarebbero state mantenute correttamente).

Inoltre potrebbe esserci un altro approccio che non è fisso nella dimensione della memoria (beh, in senso stretto, nemmeno quanto sopra), che definirebbe la significatività minima in termini di eventi / periodo (giorno, mese, anno, tutti i tempi) per cui mantenere statistiche. Ciò potrebbe garantire il massimo errore in ciascuna delle statistiche durante l'aggregazione (vedere di nuovo il round robin).


2

Che ne dici di un adattamento dell ' "algoritmo di sostituzione della pagina dell'orologio" (noto anche come "seconda possibilità")? Posso immaginare che funzioni molto bene se le richieste di ricerca sono distribuite in modo uniforme (ciò significa che la maggior parte dei termini cercati viene visualizzata regolarmente anziché 5 milioni di volte di seguito e poi mai più).

Ecco una rappresentazione visiva dell'algoritmo: algoritmo di sostituzione della pagina dell'orologio


0

Memorizza il conteggio dei termini di ricerca in una tabella hash gigante, dove ogni nuova ricerca fa sì che un particolare elemento venga incrementato di uno. Tieni traccia dei primi 20 termini di ricerca circa; quando l'elemento all'11 ° posto è incrementato, controlla se ha bisogno di scambiare le posizioni con il # 10 * (non è necessario mantenere i primi 10 ordinati; tutto ciò che ti interessa è tracciare la distinzione tra 10 ° e 11 °).

* È necessario effettuare controlli simili per vedere se un nuovo termine di ricerca si trova all'11 ° posto, quindi questo algoritmo si riduce anche ad altri termini di ricerca, quindi sto semplificando un po '.


Ti consigliamo di limitare le dimensioni della tua tabella hash. E se ottieni un flusso di ricerche uniche? Devi essere sicuro di non impedirti di notare un termine che viene cercato regolarmente ma di rado. Nel tempo questo potrebbe essere il termine di ricerca più utilizzato, soprattutto se tutti gli altri termini di ricerca sono "eventi attuali", ovvero hanno cercato molto ora, ma non così tanto la prossima settimana. In realtà, considerazioni come queste potrebbero essere approssimazioni che vorresti fare. Giustificateli dicendo che non prenderemo questo tipo di cose perché così facendo l'algoritmo è molto più costoso in termini di tempo / spazio.
cape1232

Sono abbastanza sicuro che Google abbia un conteggio di tutto : alcuni conteggi non vengono mantenuti staticamente, ma piuttosto calcolati secondo necessità.
Ether

0

a volte la risposta migliore è "non lo so".

Prenderò una pugnalata più profonda. Il mio primo istinto sarebbe quello di inserire i risultati in un Q. Un processo elaborerebbe continuamente gli elementi che entrano nel Q. Il processo manterrebbe una mappa di

termine -> conteggio

ogni volta che viene elaborato un elemento Q, è sufficiente cercare il termine di ricerca e aumentare il conteggio.

Allo stesso tempo, manterrei un elenco di riferimenti alle prime 10 voci nella mappa.

Per la voce attualmente implementata, vedere se il suo conteggio è maggiore del conteggio del conteggio della voce più piccola tra i primi 10. (se non è già presente nell'elenco). In caso affermativo, sostituire il più piccolo con la voce.

Penso che funzionerebbe. Nessuna operazione richiede molto tempo. Dovresti trovare un modo per gestire la dimensione della mappa di conteggio. ma dovrebbe essere sufficiente per una risposta all'intervista.

Non si aspettano una soluzione, vogliono vedere se riesci a pensare. Non devi scrivere la soluzione lì per lì ....


12
La struttura dei dati è chiamata a queue, Qè una lettera :).
IVlad

3
Se stessi conducendo l'intervista, "Non so <stop>" non sarebbe sicuramente la risposta migliore. Pensa in piedi. Se non lo sai, capiscilo o almeno prova.
Stephen

nelle interviste, quando vedo qualcuno con ibernazione sul suo curriculum di 7 pagine 5 volte e non può dirmi cos'è ORM, concludo immediatamente l'intervista. Preferisco non metterlo sul loro curriculum e dire semplicemente: "Non lo so". Nessuno sa tutto. @IVIad, stavo fingendo di essere uno sviluppatore C e cercavo di salvare bit ...;)
hvgotcodes

0

Un modo è che per ogni ricerca, memorizzi quel termine di ricerca e il relativo timestamp. In questo modo, trovare i primi dieci per un periodo di tempo è semplicemente una questione di confrontare tutti i termini di ricerca entro il periodo di tempo specificato.

L'algoritmo è semplice, ma lo svantaggio sarebbe un maggiore consumo di memoria e tempo.


0

Che ne dici di usare uno Splay Tree con 10 nodi? Ogni volta che provi ad accedere a un valore (termine di ricerca) che non è contenuto nell'albero, butta via una foglia, inserisci invece il valore e accedi ad esso.

L'idea alla base di questo è la stessa della mia altra risposta . Partendo dal presupposto che i termini di ricerca siano accessibili in modo uniforme / regolare, questa soluzione dovrebbe funzionare molto bene.

modificare

Si potrebbero anche memorizzare altri termini di ricerca nell'albero (lo stesso vale per la soluzione che suggerisco nell'altra mia risposta) per non eliminare un nodo a cui si potrebbe accedere di nuovo molto presto. Più valori si memorizzano in esso, migliori saranno i risultati.


0

Non so se ho capito bene o no. La mia soluzione sta usando heap. A causa dei primi 10 elementi di ricerca, creo un heap di dimensione 10. Quindi aggiorno questo heap con una nuova ricerca. Se la frequenza di una nuova ricerca è maggiore dell'heap (Heap massimo) in alto, aggiornalo. Abbandona quello con la frequenza più piccola.

Ma come calcolare la frequenza della ricerca specifica verrà conteggiato su qualcos'altro. Forse come tutti hanno affermato, l'algoritmo del flusso di dati ...


0

Usa cm-sketch per memorizzare il conteggio di tutte le ricerche dall'inizio, mantieni un min-heap di dimensione 10 con esso per i primi 10. Per il risultato mensile, tieni 30 cm-sketch / hash-table e min-heap con esso, ognuno inizia conteggio e aggiornamento dagli ultimi 30, 29 .., 1 giorno. Come un giorno, cancella l'ultimo e usalo come giorno 1. Lo stesso per il risultato orario, mantieni 60 hash-table e min-heap e inizia a contare per gli ultimi 60, 59, ... 1 minuto. Man mano che passano i minuti, cancella l'ultimo e usalo come minuto 1.

Il risultato mensile è accurato nell'intervallo di 1 giorno, il risultato orario è accurato nell'intervallo di 1 min


0

Il problema non è universalmente risolvibile quando si dispone di una quantità fissa di memoria e di un flusso "infinito" (penso molto molto grande) di token.

Una spiegazione approssimativa ...

Per capire perché, si consideri un flusso di token che ha un particolare token (cioè, parola) T ogni N token nel flusso di input.

Inoltre, si supponga che la memoria possa contenere riferimenti (id parola e conteggi) al massimo a M token.

Con queste condizioni, è possibile costruire un flusso di input in cui il token T non verrà mai rilevato se N è abbastanza grande in modo che il flusso contenga diversi M token tra T.

Questo è indipendente dai dettagli dell'algoritmo top-N. Dipende solo dal limite M.

Per capire perché questo è vero, considera il flusso in entrata costituito da gruppi di due token identici:

T a1 a2 a3 ... a-M T b1 b2 b3 ... b-M ...

dove a e b sono tutti gettoni validi non uguali a T.

Si noti che in questo flusso, la T appare due volte per ogni ai e bi. Eppure sembra abbastanza raro da essere svuotato dal sistema.

Partendo da una memoria vuota, il primo token (T) occuperà uno slot nella memoria (delimitato da M). Quindi a1 consumerà uno slot, fino ad a- (M-1) quando la M è esaurita.

Quando arriva aM l'algoritmo deve rilasciare un simbolo, quindi lascia che sia il T. Il simbolo successivo sarà b-1 che farà svuotare a-1, ecc.

Quindi, la T non rimarrà residente nella memoria abbastanza a lungo per costruire un conteggio reale. In breve, qualsiasi algoritmo mancherà un segno di frequenza locale sufficientemente bassa ma di alta frequenza globale (per tutta la lunghezza del flusso).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.