Architettura di onto-rs
Documento di architettura del linguaggio ONTO e del suo motore database
unificato. Per la sintassi del linguaggio vedi LINGUAGGIO.md;
per le prestazioni BENCHMARK.md; per i limiti operativi e
le strategie di deployment USE_CASES.md.
Panoramica
onto-rs è un sistema unificato: il linguaggio e il database sono lo
stesso motore. L'interprete possiede un OntoEngine; ogni elemento
[ V=febbre, ... ] creato da uno script inserisce automaticamente un
record nel motore trivalente. Eseguire un programma ONTO equivale a
popolare — e, tramite classi e funzioni built-in, interrogare — un database
indicizzato in memoria.
Moduli
core::tv TrivalentValue (V/F/N) — negazione asimmetrica e stretta, and/or,
conversione da valori generici
core::movp MOVP — aggregato di proprietà trivalenti, con peso epistemico
(V=2, N=1, F=0)
storage::index TrivalentIndex — indice invertito per proprietà: tre bitset
per proprietà (V, F e N assegnato; per le interrogazioni
N è !V & !F),
AND/OR/NOT a livello di parola (u64)
storage::engine OntoEngine — indice + punteggi + record + grafo relazioni,
interfaccia unica di inserimento, ricerca, ranking e
attraversamento del grafo
lang::sql SQL sulle classi: SELECT con espressioni, funzioni e
sottointerrogazioni, INSERT, UPDATE, DELETE, e lo schema
in information_schema. Ogni interrogazione ha un
risultato parziale, calcolato sui dati di un nodo, e una
fusione dei parziali
storage::radice radice di una parola (algoritmo Snowball: italiano e
altre 17 lingue), per la ricerca a testo libero
storage::attributi IndiceAttributi — indice ordinato di numeri e testi dei
record: ricerca per valore esatto, per intervallo e a
testo libero (rilevanza, radici, sinonimi, prefissi)
storage::partizionato MotorePartizionato — record in partizioni su disco,
poche caricate alla volta: per dati più grandi della RAM
storage::condiviso MotoreCondiviso — lo stesso motore condiviso tra thread:
letture in parallelo, scritture in esclusiva
graph::relations OntoGraph — archi tipati (from, rel, to) tra record del
motore, traversal 1-hop e multi-hop (BFS)
query LazyQuery — query trivalenti componibili (and/or/not) che
materializzano V/F/N solo quando letti
lang::lexer tokenizzazione (keyword, V/F/N, stringhe con
interpolazione, operatori)
lang::parser recursive descent con precedenza esplicita
lang::ast nodi dell'AST
lang::interprete interprete tree-walking, collegato a OntoEngine
Il motore: OntoEngine
TrivalentIndex mantiene, per ogni proprietà, due bitset (BitVec<u64>)
usati dalle interrogazioni: bv (dove la proprietà è V) e bf (dove è F).
Per le interrogazioni N è sempre implicito come !bv & !bf, e le
combinazioni (and, or, not, get_n) sono operatori bitwise nativi
applicati all'intero bitset — O(n/64) istruzioni macchina, non un ciclo
bit-a-bit. Un terzo bitset, bn, segna dove N è stato assegnato
esplicitamente: non entra nelle interrogazioni, serve a distinguere «vale
N» da «il record non ha la proprietà» e quindi a ricostruire un record
dall'indice.
La crescita dei bitset all'inserimento è geometrica (raddoppio della
capacità con BitVec::reserve), quindi l'inserimento sequenziale è lineare
amortizzato, non quadratico.
OntoEngine compone su questo indice:
ritirati: HashMap<usize, MOVP>— i soli record retratti, com'erano al ritiro; i record vivi non sono memorizzati e si ricostruiscono dall'indice conget;attributi: IndiceAttributi— numeri e testi dei record, in ordine;scores: Vec<i16>— peso epistemico per id, calcolato all'inserimento;graph: OntoGraph— relazioni tipate tra record, nello stesso motore.
Interfacce principali: insert (indicizza un MOVP e restituisce l'id),
search_val / search_profile (AND su più proprietà, con le proprietà
ordinate per selettività — V più raro prima), search_any (OR deduplicato
tra più profili, sempre sui bitset), top_k (selezione O(n) con
select_nth_unstable_by, non un sort completo), scan_threshold
(classificazione V/N/F per soglia sul punteggio), aggiungi_relazione /
avanti / indietro / percorso per il grafo, save / load per lo
snapshot binario su disco (l'intero stato — indice, record, punteggi,
grafo — implementa Serialize/Deserialize e viene scritto/letto con
bincode). Per non perdere le scritture tra due snapshot, MotorePersistente
(storage::persistente) avvolge OntoEngine con un log di scrittura
(storage::wal::Wal): vedi "Roadmap per la produzione" più sotto.
Mutazione e retrazione
Oltre a insert, il motore espone due operazioni di mutazione sui record
esistenti:
set_property(id, prop, tv)aggiorna una singola proprietà: sposta il bit dell'elemento tra i bitset V/F della proprietà (TrivalentIndex::update, stessa logica d'impostazione bit diinsert), ricalcola il punteggio e mantiene allineato il record. Impostare N azzera entrambi i bit (N resta implicito come!bv & !bf).retract(id)è una cancellazione logica: il record resta neirecords(marcato tombstoned, quindi ancora leggibile viaget), ma è rimosso da tutti i bitset di tutte le proprietà e non compare più insearch_val,search_profile,top_kné nelle membership di classe. L'esclusione vale anche per il caso N, che altrimenti includerebbe l'id (bit tutti a zero ⇒!bv & !bfvero): per questo l'indice mantiene un bitset di tombstone che maschera ogni calcolo di N, e il selettoreN=propdelle classi lo applica esplicitamente. Un elemento retratto non "risorge":set_propertysu un id tombstoned è un no-op.
WAL esteso e politiche di sync
Il log (storage::wal::Wal) registra, oltre a Insert e Relazione, le
mutazioni SetProp { id, prop, tv } e Retract { id }: MotorePersistente
espone i wrapper imposta_proprieta e ritira che scrivono nel log prima
di applicare, come già fa inserisci. Entrambe le operazioni sono
idempotenti (riapplicarle non cambia lo stato), quindi il replay resta
corretto anche se una coda già applicata viene rigiocata; per Insert
vale invariato il meccanismo di salto per generazione+offset del
checkpoint. checkpoint() forza inoltre un sync() completo del WAL prima
di leggerne la lunghezza, così l'offset catturato nello snapshot punta a
byte effettivamente su disco anche con sync bufferati.
La politica di sincronizzazione è configurabile (PoliticaSync):
OgniOperazione (default, invariato: flush + fsync a ogni operazione) oppure
Bufferata { ogni_n, ogni_ms }, che sincronizza ogni N operazioni o M
millisecondi oltre che su sync() esplicito — più veloce, a prezzo di una
finestra di perdita in caso di crash (vedi il trade-off documentato sul tipo).
Formato snapshot versionato
I file di snapshot (sia OntoEngine::save/load sia il checkpoint di
MotorePersistente) hanno un envelope in testa: magic number di 4 byte +
versione di formato u32 (VERSIONE_FORMATO_SNAPSHOT, attualmente 5),
seguiti dal payload bincode. In lettura, magic o versione non coincidenti
producono PersistenceError::FormatoNonSupportato invece di un errore
bincode generico su un file che non è più riconoscibile.
Query lazy (query::LazyQuery)
Comporre LazyQuery con and/or/not calcola sempre V ed F (economici,
bitwise) ma non materializza mai N finché non si chiama esplicitamente
n(): le query che servono solo "quali record soddisfano di sicuro la
condizione" — il caso comune — non pagano mai il costo del terzo bitset.
È anche il meccanismo con cui le classi mereologiche si risolvono.
L'interprete: dal linguaggio al motore
Interprete possiede Ambiente (scope a pila: la lookup risale dallo
scope corrente al globale, la scrittura avviene sempre nello scope
corrente) e un OntoEngine.
Pipeline di esecuzione di uno script
sorgente .onto
│
▼
lang::lexer ── token stream (keyword, V/F/N, stringhe, operatori)
│
▼
lang::parser ── AST (recursive descent, precedenza esplicita)
│
▼
lang::interprete (tree-walking)
├── elementi → Aggregato + insert in OntoEngine (id salvato)
├── classi → ClasseVal (selettore + eventuale snapshot fissa)
├── chiamate → dispatch: built-in motore → funzione utente
└── controllo → if/for/return con esiti di blocco
│
▼
OntoEngine (indice bitset + record + punteggi + grafo)
Come il linguaggio popola il motore
Elementi ([ valore=proprieta, ... ]): le coppie vengono valutate in
ordine e composte in un Aggregato; i campi trivalenti sono inseriti come
MOVP nel motore tramite inserisci_nel_motore, che salva l'id assegnato
nell'aggregato. Il binding = nome avviene dopo la valutazione del valore:
l'elemento nasce con un nome generico e assume il nome del binding. I campi
non trivalenti (stringhe, numeri, liste) restano nel solo aggregato.
Aggregazione (aggregate X ▷ prop): compone o sovrascrive i campi di
X (creato se assente) dalle variabili d'ambiente nominate, un campo per
istruzione, e reinserisce l'aggregato nel motore se contiene valori
trivalenti.
Come le classi lazy si risolvono sui bitset
ClasseVal tiene il selettore e, solo per class fixed, gli id congelati
a definizione. A ogni interrogazione (in, count, members, iterazione
for), valuta_selettore traduce il selettore in una LazyQuery:
Prop(V=febbre,F=dispnea,N=esame) legge i bitset V/F della proprietà dall'indice — per V=F li scambia, per V=N usa!V & !F;And/Orcombinano con AND/OR bitwise trivalenti;ClasseRefrisolve ricorsivamente l'altra classe, rifiutando i cicli (in_visita: una classe che referenzia se stessa, anche indirettamente, è un errore a runtime).
La membership usa solo il bitset V della query: i nuovi inserimenti sono
visibili automaticamente alle classi non fisse, senza invalidazione né
migrazione. class fixed salta il selettore dopo la definizione e usa
gli id congelati.
Dispatch delle chiamate
nome{valore=campo, ...} cerca prima una funzione built-in del motore
(search_val, search_profile, top_k, distribution, add_relation,
forward, backward, count, members); in assenza, una funzione utente
definita con def. Gli argomenti from/to delle relazioni accettano sia id
interi sia elementi (ne usano l'id del motore).
Controllo di flusso
return propaga come esito di blocco risalendo attraverso if/for
annidati fino alla chiamata di funzione più vicina: dentro un ciclo esce
dalla funzione, non solo dal ciclo.
Decisioni di design
| Decisione | Rationale |
|---|---|
| In-memory durante l'esecuzione, persistenza a snapshot manuale | Le query del dominio (congiunzioni, distribuzioni, ranking) sono compute-bound su strutture compatte; il disco aggiungerebbe latenza a ogni scrittura senza vantaggi nel caso d'uso target. OntoEngine::save/load (bincode) coprono il caso "stato ricostruibile tra riavvii"; un log di scrittura continuo resta demandato al sistema ospitante (roadmap) |
| Nessuna sincronizzazione dentro il motore | OntoEngine non ha stato interno mutabile nascosto: le letture prendono &self e sono sicure in parallelo. La sincronizzazione sta fuori, in MotoreCondiviso, e non costa nulla a chi usa il motore da un solo thread. Le singole operazioni sui bitset non sono parallelizzate: sono già limitate dalla memoria |
| Bitset a parola intera invece di indici per valore | O(n/64) istruzioni per combinazione; misurato 41-51× più veloce di SQLite su AND multi-proprietà a 500 000 record |
N implicito (!V & !F) |
Un terzo bitset per proprietà raddoppierebbe memoria e banda; N si materializza solo on-demand nelle LazyQuery |
| Nessuna copia dei record | L'indice tiene per ogni proprietà tre bitset (V, F, N assegnato): un record si ricostruisce da lì quando serve, in tempo proporzionale al numero di proprietà. Un record costa così tre bit per proprietà più due byte di peso epistemico, invece di centinaia di byte. Si conservano per intero solo i record retratti |
| Grafo integrato nello stesso motore | Relazioni e proprietà vivono nello stesso spazio di indirizzi degli id: nessun sistema separato da sincronizzare |
| Interprete tree-walking | Semplicità e leggibilità; il carico pesante (query) sta nel motore, non nell'interpretazione |
Campi aggregato in Vec ordinato |
Preserva l'ordine di dichiarazione nella rappresentazione testuale |
Roadmap per la produzione
Evidenziati anche in USE_CASES.md:
-
Persistenza: due meccanismi componibili, entrambi pensati per restare corretti anche se il processo termina a metà scrittura.
OntoEngine::save/load(bincode, esposti daonto run --save/--loade dalla REPL con:save/:load): snapshot binario completo dell'indice, dei record, dei punteggi e del grafo, preceduto da un envelope magic number + versione di formato (lettura di una versione non supportata →FormatoNonSupportatoesplicito).savescrive prima su un file temporaneo, lo forza su disco (fsync) e solo allora lo sostituisce (rename, atomico) al path finale: un crash a metà non lascia mai un file troncato al suo posto.loaddeserializza con un limite di byte configurato (16 GiB): senza, un file di stato corrotto o sbagliato farebbe allocare bincode secondo un campo di lunghezza letto dal file stesso, prima di verificarne la plausibilità.storage::wal::Wal+storage::persistente::MotorePersistente: log di scrittura append-only (con politica di sync configurabile, default afsyncper ogni operazione) che registra ogniinsert/aggiungi_relazione/imposta_proprieta/ritirae le rigioca all'apertura sopra l'ultimo checkpoint, così che un riavvio (o un crash) tra duecheckpointnon perda le operazioni intermedie. Il checkpoint (snapshot + generazione/posizione nel WAL già catturate) è scritto con lo stesso schema atomico diOntoEngine::save; il troncamento del log che segue è solo un'ottimizzazione di spazio, non una condizione di correttezza —Wal::replayriconosce dal cambio di generazione del file (un identificatore scritto in testa a ogni file ricreato da zero) quali byte sono successivi al checkpoint, invece di fidarsi della sola lunghezza: un file appena troncato può ricrescere oltre la posizione registrata dal checkpoint precedente, e un confronto di lunghezze lo scambierebbe per il vecchio, saltando per errore operazioni nuove. È esposto daonto run --store <dir>eonto repl --store <dir>tramiteInterprete::apri_persistente: lo stato è ripristinato all'avvio, ogni elemento e ogni relazione creati da uno script sono registrati nel log prima di entrare nel motore, e un checkpoint è scritto a fine esecuzione (nella REPL con:checkpoint). Accanto allo snapshot un registro (script_applicati) associa l'impronta di ogni script eseguito agli id degli elementi che ha creato: rieseguire lo stesso sorgente riusa quei record invece di inserirne di nuovi, e non duplica le relazioni già presenti. Ancheset_property,retract, gli attributi numerici e testuali e le transazioni passano dal log: una transazione è scritta come una voce sola, che al riavvio è rigiocata per intero o scartata.
-
Concorrenza:
MotoreCondivisocondivide unOntoEnginetra thread con un lock a grana grossa: molte letture insieme, una scrittura alla volta, e ogni lettura vede lo stato prima o dopo una scrittura, mai a metà. Con 500 000 record e una congiunzione di tre proprietà le interrogazioni al secondo passano da circa 3 400 con un thread a circa 6 800 con due e 11 200 con quattro, e non crescono oltre i core fisici (misura riproducibile con il testmisura_letture_parallele). Le scritture non scalano: un carico dominato dalle scritture va partizionato su più istanze. L'interprete del linguaggio resta a thread singolo: il parallelismo riguarda chi usa il motore come libreria. -
Servizio di rete (
onto serve): un interprete e il suo motore esposti su HTTP o HTTPS.-
Richieste:
POST /esegue il sorgente ONTO nel corpo e risponde con ciò che ha stampato (400 e messaggio in caso di errore);POST /leggilo esegue senza lasciare effetti;GET /dà il numero di record;GET /statoruolo, epoca e posizione del nodo;POST /checkpointcompatta il diario. -
Accesso e cifratura: con
--tokenogni richiesta portaAuthorization: Bearer <token>. Con--tls-certe--tls-keyil servizio parla TLS (rustls); con--tls-caanche i collegamenti verso gli altri nodi sono cifrati e verificati. -
Letture in parallelo: con
--workers nci sono n interpreti in più, ognuno tenuto al passo con il diario, che servonoPOST /leggiinsieme. Le scritture restano una alla volta sull'interprete principale; una lettura vede sempre le scritture già confermate. -
Persistenza: lo stato di un nodo è un'istantanea dell'interprete (motore, variabili, funzioni, classi) più il diario dei sorgenti scritti dopo. Ogni scrittura è sul disco prima della risposta. Ogni
--compact-everysorgenti il diario viene compattato in una nuova istantanea. -
Replica: il primario numera i sorgenti e manda a ogni altro nodo quelli che gli mancano, dopo avergli chiesto a che punto è. I sorgenti sono deterministici, quindi tutti i nodi attraversano gli stessi stati. A un nodo troppo indietro, appena aggiunto o il cui diario ha preso un'altra strada, il primario manda l'istantanea e poi i sorgenti successivi. Su una replica i sorgenti dei client non lasciano effetti.
-
Sostituzione del primario: con
--node <indirizzo>il nodo fa parte di un gruppo (gli altri si indicano con--replica). Le repliche sorvegliano il primario; se non risponde, una replica chiede il voto alle altre per una nuova epoca e con i voti della maggioranza diventa primario. Ogni nodo vota una volta per epoca, e solo per chi ha un diario almeno aggiornato quanto il suo: in un'epoca c'è al più un primario, e un sorgente confermato alla maggioranza non va perso. Un primario scrive solo se raggiunge la maggioranza del gruppo, e torna replica appena scopre un'epoca più recente. -
SQL in rete:
POST /sqlesegue un'istruzione SQL e risponde in JSON (colonne,righe,comando; i valori di verità sonotrue,false,null). Con--pg-portil servizio parla anche il protocollo di PostgreSQL: i suoi client e driver si collegano, con il token come parola d'ordine, e mandano interrogazioni semplici o preparate con parametri. Un'istruzione SQL che scrive entra nel diario come ogni altro sorgente, quindi è replicata e sopravvive al riavvio. TraBEGINeCOMMITle scritture restano sospese: ognuna è provata sullo stato corrente più quelle che la precedono, senza lasciare effetti, e alla conferma entrano nel diario come una sola transazione;ROLLBACKle lascia cadere. -
Ripartizione dei dati (
onto route): un instradatore davanti a più gruppi, ognuno con una parte dei dati. Una richiesta con l'intestazioneX-Onto-Chiaveva al gruppo scelto dalla chiave; unaINSERTsenza chiave usa il valore della sua prima colonna. Ogni elemento ricorda la chiave con cui è nato (proprietà_chiave); quelli senza chiave, scritti a tutti i gruppi, stanno in copia in ognuno e nelle interrogazioni contano una volta. Gruppi diversi scrivono insieme, e le scritture trovano da sole il primario di ogni gruppo. -
Scritture su più gruppi: un sorgente senza chiave (classi, funzioni, vincoli,
UPDATE,DELETE) o una transazione SQL sono applicati da tutti i gruppi coinvolti o da nessuno, in due tempi. Prima ogni gruppo prova il sorgente senza lasciare effetti e si impegna a non scrivere altro; se tutti riescono, tutti confermano, altrimenti tutti annullano. I sorgenti sono deterministici, quindi alla conferma l'esito è quello della prova. Se a un gruppo la conferma non arriva, chiede agli altri: se uno ha confermato conferma anche lui, se uno non ne sa nulla annulla, se sono tutti in attesa aspetta. La scrittura preparata è conservata su disco e mandata alle repliche del gruppo: se il primario cade, chi ne prende il posto la riprende e ne chiede la sorte. Il sorgente confermato entra nel diario con il suo identificativo, così nessun nodo lo applica due volte. -
Campi unici tra i gruppi: il gruppo che prova una scrittura dice quali valori ha scritto in campi unici. Prima della conferma ogni altro gruppo controlla di non averli e si impegna a sua volta a non scrivere altro; se uno li ha, tutti annullano. Controllo e impegno sono dei gruppi, non dell'instradatore: valgono per ogni genere di scrittura e con più instradatori. Una scrittura a un solo gruppo che non tocca campi unici è applicata subito, senza secondo tempo.
-
Letture su più gruppi: una
SELECTsu una tabella è calcolata in parte da ogni gruppo (righe, oppure gruppi con lo stato degli aggregati) e fusa dall'instradatore, che riunisce i gruppi con la stessa chiave, conclude gli aggregati, applicaHAVING, calcola le funzioni a finestra, ordina e taglia. UnaSELECTche accoppia righe (JOIN, sottointerrogazioni) può accoppiarne di gruppi diversi. Se si può, l'instradatore la esegue per diffusione: lascia nei gruppi la tabella più grande, raccoglie le altre e le dà a ognuno; ogni gruppo unisce la sua parte della prima con quelle, intere, e restituisce un parziale, che si fonde come per una tabella sola. Le righe di una tabella sono così accoppiate una volta sola, e quelle della tabella grande non lasciano i gruppi. Altrimenti (la stessa tabella due volte, l'iddegli elementi, piùSELECTinUNION) l'instradatore chiede a ogni gruppo gli elementi di tutte le tabelle che nomina, e la esegue in un interprete d'appoggio. Ogni gruppo applica prima le condizioni che riguardano una sola tabella e manda solo gli elementi che restano; e se la tabella si unisce per uguaglianza a una già raccolta, solo quelli il cui valore compare tra i raccolti. In entrambi i casi il risultato è quello di un nodo solo con tutti i dati. Dentro una transazione aperta ogni gruppo applica prima, senza effetti, le scritture sospese che lo riguardano: le letture le vedono. Un sorgente ONTO mandato aPOST /leggisenza chiave è invece eseguito da ogni gruppo, e le risposte sono accostate. -
Crescita (
onto split): le chiavi sono divise tra i gruppi per resto dell'impronta. Aggiungere un gruppo taglia a metà la parte di uno solo: il gruppo nuovo riceve lo stato intero di quello da cui nasce, poi ognuno dei due ritira gli elementi le cui chiavi toccano all'altro. Gli altri gruppi non si muovono. Con--routerla divisione la fa l'instradatore in funzione: aspetta le richieste in corso, trattiene le nuove finché ha finito, poi le serve con la disposizione nuova. -
Disposizione dei gruppi: quali nodi formano ogni gruppo e quale parte delle chiavi gli tocca. Ha un numero, che cresce a ogni divisione, e i nodi la ricordano. Ogni richiesta di un instradatore porta il numero della disposizione con cui è stata instradata; un nodo che ne conosce una più recente la rifiuta, e l'instradatore legge dai nodi quella nuova e riprova. Chi divide un gruppo avvisa prima i nodi che una divisione è in corso, e gli altri instradatori aspettano che finisca. Per questo una divisione vale per tutti gli instradatori, anche per quelli avviati dopo con gli argomenti di prima.
Restano fuori: durante una divisione le richieste aspettano; in un
JOINtra gruppi le tabelle diverse dalla più grande vanno raccolte e date a ogni gruppo; ogni scrittura su più gruppi è provata su una copia dello stato, con un costo che cresce con i dati; i riferimenti tra tabelle sono controllati prima della scrittura, non insieme a essa. -
-
Memoria su file (
--memory-file): un allocatore che prende la memoria del processo da un file mappato. Le pagine di un file mappato sono recuperabili: quando la RAM non basta il sistema le scrive sul file e le rilegge al bisogno, quindi motore, indici e variabili possono superare la memoria fisica, fino alla dimensione del file. Vale per tutto il sistema senza modifiche al motore; il costo è il tempo, quando i dati in uso non stanno in memoria. Per ritardare quel momento il sistema tiene in memoria il meno possibile: uno script è mappato dal disco, analizzato ed eseguito un'istruzione di primo livello alla volta dopo un controllo della sintassi dell'intero file; ogni testo sta in memoria una volta sola, e l'indice delle parole occupa 8 byte per ogni parola distinta di ogni testo. Conta poi quali pagine restano in memoria. L'allocatore dà a ogni dimensione di blocco zone proprie del file, così i dati dello stesso genere stanno vicini: le pagine con i testi, scritti una volta e riletti di rado, si possono rilasciare senza portarsi via i nodi degli indici, che servono a ogni inserimento. Il motore stesso, quando inserisce, aggiunge in coda: gli attributi e le lunghezze dei testi stanno in strutture ordinate per id, che crescono da un lato solo, e l'ordine dei testi per valore, che andrebbe toccato in un punto diverso a ogni inserimento, si costruisce la prima volta che una ricerca o un vincolo di unicità lo chiede. Un carico più grande della RAM tiene così in memoria solo la coda di ogni struttura. Anche le letture restano vicine: scorrere una tabella legge gli elementi nell'ordine in cui sono stati scritti, che è quello in cui stanno sul file, e tiene pronti solo gli ultimi letti; un confronto su una colonna con indice legge dell'indice solo la parte che gli serve. -
Ricerca per vicinanza e per significato: una lista di numeri è un vettore, e
nearestconfronta quello dato con tutti.vector_indexaggiunge un indice dei vicini, un grafo a più strati (HNSW) in cui ogni vettore è collegato ai suoi prossimi: la ricerca scende dagli strati alti, radi, a quello di base, e ne visita una piccola parte. L'indice segue ogni modifica ed è salvato con lo stato.similar_textricava il significato dai testi stessi: ogni radice ha un vettore sparso suo, scelto dalla radice; il vettore di contesto di una parola è la somma di quelli delle parole che le stanno intorno in tutti i testi, pesati per rarità; il vettore di un testo è la somma pesata di quelli delle sue parole. Parole che compaiono negli stessi contesti hanno vettori vicini. Questo modello non fa parte dello stato salvato: si ricostruisce dai testi. -
Tooling: formatter, language server, pacchetto della CLI per distribuzione binaria.