9.2 KiB
Visual Index v1
Problème et frontière
Le Visual Index transforme une collection homogène de FeatureSet READY en
candidats de recherche. Il consomme exclusivement feature_set_id et les
descripteurs ORB lus par le Feature Reader. Il ne fait ni matching final, ni
ratio test, ni vérification géométrique.
Choix algorithmique
La v1 utilise un LSH binaire déterministe à six tables. Chaque table extrait
24 positions distinctes des 256 bits ORB. La position v1 est
(41*table + 11*bit) mod 256; 11 étant premier avec 256, les 24 positions
d'une table sont distinctes. Une clé est (table_id, key24). Des descripteurs proches en
Hamming ont une probabilité élevée de collision dans au moins une table, sans
conversion flottante.
Alternatives évaluées :
- le hash exact est très compact et déterministe, mais son rappel s'effondre dès qu'un descriptor varie d'un bit ;
- le multi-index hashing avec sous-chaînes et multiprobes offre des garanties Hamming intéressantes, mais le nombre de postings/probes nécessaire au rappel utile d'ORB est trop élevé pour une v1 bornée ;
- FLANN-LSH masque son format, ses allocations et sa stabilité de sérialisation, ce qui nuit à la reprise et à l'audit ;
- BoW/IVF donne un bon retrieval image, mais impose vocabulaire, entraînement, identité et politique de mise à jour avant de pouvoir être incrémental ;
- HNSW et FAISS ajoutent une dépendance et un état mutable complexes sans avantage décisif à quelques milliers d'images.
Ce LSH n'est pas un matcher. Il privilégie une base déterministe, segmentable
et contrôlable. Une évolution de la sélection de bits exige une nouvelle
visual_index_version.
Identité et configuration
Le kind est orb-lsh, version 1. Un index contient exclusivement des Feature
Sets de même descriptor_type, dimension, extractor_kind, version et
parameter_fingerprint. Sa configuration v1 contient :
table_count=6;key_bits=24;max_features_per_setentre 1 et 1024, défaut 512 ;max_bucket_postingsentre 1 et 4096, défaut 256 ;max_segments=256;max_feature_sets_per_segment=16.
Le fingerprint de paramètres est SHA-256 des 32 octets canoniques
L3DVICF1, version et cinq entiers little-endian. Aucun padding, JSON, locale
ou endianness hôte n'intervient. visual_index_id est une identité SQLite
AUTOINCREMENT, jamais réutilisée après publication.
Échantillonnage
Au plus max_features_per_set features sont indexées. La sélection v1 retient
le préfixe de feature_index croissant. Les postings conservent l'indice
original. Un Feature Set vide est
membre valide sans posting. Le build ne garde qu'un Feature Set et une tranche
Feature Reader de 256 descripteurs en mémoire.
Segments et Project Database v6
Un index logique possède des segments immuables READY. Chaque update publie un
segment de un à seize nouveaux Feature Sets, puis ajoute atomiquement segments
et memberships. UNIQUE(visual_index_id,feature_set_id) assure l'idempotence.
Une recherche copie au début la liste bornée des segments READY, relâche le
mutex DB, puis lit ce snapshot. Un segment commité au milieu sera visible à la
requête suivante.
SQLite conserve les tables visual_indexes, visual_index_segments,
visual_index_memberships et visual_index_update_tasks. Les gros postings
restent hors DB. La configuration et chaque membership sont immuables. La
compaction est NOT_YET_WIRED; au-delà de 256 segments une update est refusée avec
LARDON3D_VISUAL_INDEX_LIMIT. Avec seize membres par segment, la capacité v1 est donc
exactement 4096 Feature Sets par index. Le refus ne publie ni segment ni membership et
l'index existant reste requêtable.
Le DDL v6 exact est schema_visual_v6 dans src/project_db.c. Il impose
AUTOINCREMENT aux index/segments, les uniques
(visual_index_id,generation), (visual_index_id,sha256) et
(visual_index_id,feature_set_id), ainsi que les FKs vers index, Feature Set,
segment et tâche. Les CHECKS bornent tables 1..32, bits 8..32, sampling
1..1024, bucket 1..4096, membres segment 1..16 et durabilité 0..1. La migration
entière reste sous BEGIN IMMEDIATE et possède une injection de rollback v6.
Segment File v1
Le fichier est little-endian et ne sérialise aucune structure C. Layout :
| Offset | Taille | Champ |
|---|---|---|
| 0 | 8 | magic L3DVIDX\0 |
| 8 | 4 | format version 1 |
| 12 | 4 | header size 128 |
| 16 | 4 | table count |
| 20 | 4 | key bits |
| 24 | 8 | posting count |
| 32 | 8 | member count |
| 40 | 8 | postings offset, 128 |
| 48 | 8 | total size |
| 56 | 32 | index parameter fingerprint |
| 88 | 32 | feature parameter fingerprint |
| 120 | 8 | réservés, zéro |
Chaque posting fait 24 octets : table_id:u32, key24:u32,
feature_set_id:u64, feature_index:u32, réservé zéro u32. L'ordre est
table_id, clé, Feature Set, feature index. Le fichier exact est SHA-256 et
vit sous assets/visual-index/<2 hex>/<sha256 lowercase>.
Publication : temporaire local, écriture, fsync, hash, link sans
écrasement, validation d'une adoption concurrente, fsync du répertoire, puis
transaction DB. Un échec après publication peut laisser un orphelin mais jamais
un segment READY partiel. La durabilité distingue DURABLE et
PUBLISHED_NOT_DURABLE.
Le reader vérifie le SHA avant le parsing. Un fichier au SHA et aux métadonnées cohérents
mais portant une version future produit UNSUPPORTED_VERSION; les comptes et produits
d'offset invalides produisent CORRUPT avant allocation, conversion ou lecture de posting.
Recherche, score et bornes
L'API est centrée sur (visual_index_id, query_feature_set_id). Elle accepte
ANY_SCANSET, SAME_SCANSET ou OTHER_SCANSETS, l'exclusion du même asset,
un minimum de preuves et top_k entre 1 et 256. Elle ne retourne jamais le
Feature Set ni l'image de requête.
Une preuve est un feature_index de requête distinct ayant au moins une
collision avec le candidat. Plusieurs tables, postings ou descriptors du
candidat ne multiplient pas cette preuve. Le score final vaut
evidence_count / sampled_query_feature_count dans [0,1]. Le volume du
candidat ne peut donc pas augmenter le score sans preuve distincte. L'ordre est
score décroissant, preuves décroissantes, image_id, puis feature_set_id.
La burstiness est bornée par une contribution maximum par feature de requête
et candidat. Une première passe additionne la fréquence d'un bucket sur tous
les segments du snapshot. Au-delà de max_bucket_postings, il est ignoré : un motif
très commun ne peut ni allouer une liste géante ni dominer le score. Le reader
lit au plus 256 postings par appel. L'accumulateur contient au plus 4096
candidats ; les nouveaux candidats sont ignorés après saturation, de manière
déterministe par l'ordre des postings. Aucun cache global n'existe et un seul
segment est ouvert à la fois.
Deux updates concurrentes peuvent sélectionner le même lot et construire le même asset. La transaction SQLite et les contraintes uniques ne laissent publier qu'un segment et un membership par Feature Set; l'autre update échoue/rejoue en no-op. Une query prend son snapshot de métadonnées avant les lectures et ouvre/ferme un seul segment à la fois, y compris avec 250 à 256 segments : le nombre de descripteurs de fichier reste borné.
Tâche, reprise et ressources
visual_index.update, version 1, persiste visual_index_id et un curseur
after_feature_set_id. Une séquence traite au plus seize Feature Sets non
indexés, publie et commit un segment, puis checkpoint. Pause et annulation sont
coopératives entre lectures et avant publication ; un segment déjà READY reste
valide. La reprise recommence au dernier curseur commité et l'unicité des
memberships rend le rejeu idempotent.
L'estimation réserve un thread CPU, un slot I/O, GPU zéro, 8 Mio fixes et 2 Mio
par Feature Set, lot 1..16. record_batch reçoit le nombre de Feature Sets
réellement commités, la durée réelle et peak_memory_bytes=0 (inconnue).
Complexité et limites
Pour D descriptors échantillonnés, construction et disque sont O(6D).
Une requête effectue O(6Q log P + H) par segment (Q<=1024, H hits bornés),
pas O(images²). La mémoire build est bornée par huit métadonnées, 256
descripteurs et les postings d'un segment; la mémoire query par 4096 candidats,
256 postings et 256 résultats. À 3700 images et 512 features, environ 11,4
millions de postings sont produits. Un test structurel persiste 50 000 Feature Sets puis
confirme la pagination par 16 et le refus propre après 4096 memberships. Un index unique
ne couvre donc pas encore 50 000 images : le risque principal est le nombre de segments
et les seeks. Une compaction/base+delta ou une évolution v2 sera nécessaire, sans changer
les identités durables; elle est NOT_YET_WIRED.
Les fixtures de validation incluent un Feature Set vide, un crop réel, une rotation de 8 degrés, deux campagnes, un asset source partagé et une attaque de motif répétitif. Ces tests valident le classement de candidats LSH, jamais une compatibilité géométrique.
Frontière future
Le Candidate Pair Generator pourra filtrer sur score et evidence_count, puis
transmettre feature_set_id + feature_index au futur matcher. Le score Visual
Index ne constitue jamais une preuve géométrique.