Une seule résolution de répertoire.
Un seul parcours contigu.
La recherche vectorielle filtrée est généralement une dispersion-collecte sur un index qui ignore votre filtre. Telys contrôle la disposition physique à la place : sur la clé de partition, les voisins-les-plus-proches-où-clé-égale-x représentent une résolution de répertoire en O(1) suivie d'un seul parcours séquentiel d'un bloc contigu. Cette page décrit le runtime livré aujourd'hui, puis l'architecture vers laquelle il converge — identifiée comme spec.
Le moteur contrôle l'emplacement de chaque vecteur.
Une collection déclare partition_by à la création, et la disposition suit la clé. Le segment de base est ordonné par partition — chaque clé correspond à une tranche contiguë — et les écritures récentes se trouvent à côté dans un delta mutable de forme Arrow.
Ordonné par partition
Vecteurs stockés triés par clé de partition, de sorte que chaque partition forme un bloc contigu. Une requête délimitée lit une seule séquence mémoire, non des lignes dispersées dans un index.
O(1) clé → tranche
Un répertoire associe chaque valeur de partition à son (offset, length) dans le segment de base. La résolution du filtre est une recherche dans un dictionnaire, non un parcours d'index.
Écritures de forme Arrow
add et upsert s'ajoutent ici, chaque ligne estampillée d'un LSN d'écriture monotone. Le delta est interrogeable immédiatement et s'unit à la tranche de base via un seul chemin de parcours.
Ce qu'exécute une requête délimitée.
col.search(qvec, where={"tenant_id": "acme"}, top_k=10)
Le filtre désigne la clé de partition avec laquelle la collection a été créée.
Résolution en O(1) : la valeur de partition est résolue en (offset, length) dans le segment de base. Aucune génération de candidats, aucun point d'entrée de graphe.
Scoring exact SIMD sur un seul bloc séquentiel. Le rappel sur ce chemin est de 1,0 — c'est un parcours, non une approximation. Les p50/p95 mesurés pour cette étape sont publiés sur la page des benchmarks, chacun lié à son seuil de recall et à son rig.
Les lignes partageant la même clé dans le delta de forme Arrow sont scorées par le même chemin. Une écriture est visible dès la requête suivante.
Les hits sont filtrés au LSN du snapshot : les versions tombstonées et supersédées ne remontent jamais. Le top-k est retourné avec le payload explain attaché.
Les partitions surdimensionnées empruntent un fork déclaré : build_ivf place un IVF par partition sur toute partition dépassant le seuil de lignes, et les requêtes y sont routées avec un rerank exact — calibré contre un plancher de rappel, et nommé dans le plan.
Chaque résultat nomme son plan.
explain=True retourne le plan physique effectivement emprunté par la requête. Sur la clé de partition, vous obtenez le scan par tranche contiguë. Les partitions surdimensionnées déclarent le fork IVF. Les filtres hors clé se rabattent sur le scatter-gather — et le payload indique pourquoi.
hits = col.search(qvec, where={"tenant_id": "acme"}, top_k=10, explain=True)
hits["explain"]["plan"]
# on the partition key → "PartitionSliceExactF32" # one contiguous slice, exact, recall 1.0
# oversized partition → "PartitionIVFRerankF32" # per-partition IVF + exact rerank
# off-key filter → "ScatterGatherExact" # fallback — and it says why:
hits["explain"]["fallback_reason"]
# "path is not the physical partition key"Rien n'est écrasé. Les versions sont supersédées.
Le modèle d'écriture est un MVCC append-only. upsert écrit une nouvelle version d'un id logique et marque la précédente comme supersédée ; delete place un tombstone à un LSN de suppression ; snapshot() ancre une vue de lecture cohérente ; compact() fusionne le delta dans la base et supprime ce qu'aucun snapshot ne peut voir.
col.upsert(vectors, ids=ids, metadata=metadata) # a new version; the prior one is superseded col.delete(["doc-41"]) # tombstone at a delete LSN — no in-place erase lsn = col.snapshot() # pin a consistent read view at an LSN col.compact() # fold delta into base; drop superseded rows col.build_ivf(min_rows=20000, target_recall=0.98)
| Appel | Rôle |
|---|---|
| ajouter / ajouter_textes | Insère de nouvelles lignes. Les ids en double sont rejetés — une deuxième ligne physique pour un id logique requiert upsert, délibérément. |
| insérer / insérer_textes | Écrit une nouvelle version d'un id logique ; la version précédente est supersédée, jamais écrasée en place. |
| rechercher / rechercher_texte | Top-k filtré. explain=True attache le plan physique au résultat. |
| delete | Pose un tombstone sur les lignes logiques à un LSN de suppression. |
| compact | Fusionne le delta dans le segment de base ; supprime les versions tombstonées et supersédées. |
| build_ivf | Construit un IVF par partition sur les partitions dépassant le seuil de lignes, calibré sur un plancher de rappel. |
| snapshot | Retourne un LSN qui ancre une vue de lecture cohérente. |
| save / stats | Persiste la collection sur disque ; rapporte le nombre de lignes, les partitions et les caractéristiques de disposition. |
Tout ce qui suit est une spécification, non une référence d'API livrée. C'est le substrat vers lequel le runtime converge. La couture côté SDK est figée : la substitution sous-jacente est invisible pour les appelants.
Un planificateur. Un IR plat. Un exécuteur.
Trois front ends se réduisent en un seul planificateur, qui émet une représentation intermédiaire physique plate unique, exécutée par un exécuteur vectorisé Mojo directement sur des buffers Arrow.
Le modèle mémoire (remember · recall · as_of), l'API de requête (point_get · scan · search · hybrid_search) et le fabric. Un contrat d'entrée unique ; aucun moteur par API.
Planification logique et physique en un seul endroit. La sélectivité est estimée à partir des statistiques de segment ; le plan est explicite et retourné avec le résultat.
Un tableau plat d'opérateurs liés par des slots entiers. Le registre inter-opérateurs est un ensemble de lignes — un bitmap, des row-ids triés ou un vecteur de sélection.
Exécution vectorisée sur des buffers Arrow et des tableaux de candidats FAISS. Le chemin chaud SIMD reste hors de Python ; les pages Parquet compressées sont d'abord décodées dans des buffers temporaires.
La couche de mutation
Un write-ahead log pour la durabilité et l'ordonnancement, et un delta mutable de forme Arrow pour une visibilité immédiate — la couche table que Parquet ne fournit pas seul.
Scellés, immuables
Segments colonaires compressés et durables. Les statistiques de footer pilotent l'élagage des segments et des row-groups ; les fichiers scellés ne sont jamais mutés.
FAISS ANN
Index FAISS par segment, liés au checksum et à la version du segment, mappés en lecture seule avec une durée de vie attachée au snapshot de lecture.
Élagage et lexical
Zone maps, filtres de Bloom et bitmaps pour l'élagage ; un sidecar BM25 pour la récupération sparse et la fusion de scores.
SCAN FILTER PROJECT POINT_GET RANGE_GET AGGREGATE TOP_K HASH_JOIN HASH_GROUP_BY SORT ANN_SEARCH SPARSE_SEARCH FUSE RERANK MATERIALIZE
WAL en premier. Visible immédiatement. Scellé en arrière-plan.
Trames append-only avec checksums et LSNs monotones. La récupération rejoue les enregistrements au-delà du dernier LSN scellé et tronque au premier checksum invalide.
L'écriture atterrit dans le delta mutable et est immédiatement interrogeable — segments scellés et delta s'unissent via un seul chemin de scan.
Passé un seuil de taille, de lignes ou d'ancienneté, le delta est trié et écrit comme segment Parquet avec son .vidx et ses sidecars, puis publié par un échange atomique de manifest.
Les fusions en arrière-plan regroupent les petits segments, suppriment les lignes marquées tombstone et reconstruisent les sidecars — à budget contrôlé, pour que les déploiements on-device restent discrets.
Les segments scellés ne sont jamais mutés. Une lecture épingle (snapshot de manifest, LSN) ; les writers scellent de nouveaux segments et échangent le manifest sans perturber les lectures en cours. Le garbage collection attend le snapshot live le plus ancien.
Le filtre choisit le plan, pas l'inverse.
L'ANN filtré est une planification adaptative par sélectivité : le planificateur estime le nombre de lignes survivant au filtre à partir des statistiques de segment, puis choisit la stratégie la moins coûteuse qui respecte le contrat de rappel.
Préfiltre → exact
Prefilter scalaire ou bitmap d'abord, puis scoring SIMD exact sur les survivants. En dessous d'un seuil de lignes, l'ANN est entièrement ignoré — scan exact, rappel 1,0.
Parcours par allow-bitmap
La traversée ANN porte un allow-bitmap, de sorte que l'index ne remonte que les lignes admises par le filtre.
ANN → post-filtre
ANN en premier avec sur-fetch adaptatif, puis post-filter. Le filtre élimine peu, donc la génération de candidats mène.
Les contrôles de correction précèdent tout chiffre de performance.
Le runtime livré est vérifié par des suites de parité exécutées contre les deux implémentations du moteur, et chaque requête peut nommer le plan physique emprunté. Tel est l'ordre des opérations ici : correction d'abord, mesure ensuite.
Les benchmarks sont publiés sous un contrat d'équité — rappel apparié, matériel apparié, résultats séparés par transport, victoire / égalité / défaite rapportés. Les premiers résultats mesurés sont désormais disponibles sur la page des benchmarks, chaque chiffre accompagné de ses conditions complètes : matériel, jeu de données, dimension, sélectivité, recall et transport. Tout multiplicateur reste une hypothèse en dehors des conditions énoncées à ses côtés.