Lección 3: El índice y la búsqueda
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
El índice de voicings en producción de GA contiene 313.047 voicings de guitarra, bajo y ukelele. Construirlo lleva minutos y un clon del repositorio entero, así que esta lección construye uno pequeño con el mismo código: todos los voicings de tres notas o más en los tres primeros trastes de una guitarra. El archivo tiene el mismo formato, la misma cabecera y la misma búsqueda, y cada número de esta lección sale de él.
Todos los enlaces a GA apuntan al commit a826864. Las salidas proceden de:
dotnet run --project code/ga-ai/GaAi -c Release -- l3El corpus
Sección titulada «El corpus»VoicingGenerator.GenerateAllVoicingsAsync desliza una ventana de unos pocos trastes a lo largo del mástil y produce cada combinación de cuerdas tocadas y silenciadas. Cada voicing pasa luego por los tres pasos de la lección 2, las mismas llamadas que el bucle de exportación de GA en FretboardVoicingsCLI, sin su eliminación de duplicados:
== Corpus: GA's VoicingGenerator, standard tuning, 3 frets, window 3, at least 3 notesvoicings 15360 with 3 notes 1280 with 4 notes 3840 with 5 notes 6144 with 6 notes 4096first three, as generated:diagram MIDI (in order) GA name doc root0-0-0-x-x-x 64 59 55 Em/G E1-0-0-x-x-x 65 59 55 G7(shell) F2-0-0-x-x-x 66 59 55 Gmaj7(shell) F#Los recuentos sirven de comprobación: un mástil de 3 trastes tiene los trastes 0 a 3, cuatro opciones por cada cuerda tocada, así que los voicings de seis notas son 4⁶ = 4096, y los de cinco notas 6 × 4⁵ = 6144, seis formas de elegir la cuerda silenciada. Los diagramas empiezan por la cuerda 1, la E aguda, y la “doc root” vuelve a ser la nota más aguda de cada forma. La skill optic-k-rebuild de GA da la escala del índice real: unos 667.000 voicings de guitarra en bruto, 298.000 tras eliminar duplicados, unos 140 segundos.
El formato del archivo
Sección titulada «El formato del archivo»OptickIndexWriter recibe una lista de VoicingEntry(float[] Embedding, string Diagram, string Instrument, int[] MidiNotes, string? QualityInferred) y escribe un archivo. El curso vuelve a leer su cabecera con un BinaryReader, campo a campo, en el orden en que los escribe WriteHeader:
== The header OptickIndexWriter wrote (little-endian)magic OPTKformat version 4header size 616schema hash 0x37CD8ECFendian marker 0xFEFFdimension 124voicings 15360instruments 3 guitar offset 123496, count 15360 bass offset 7742056, count 0 ukulele offset 7742056, count 0metadata offsets at 616vectors at 123496 (15360 x 124 x 4 bytes = 7618560)metadata at 7742056 (1233243 bytes of msgpack)sqrt weights, first dims 0.67 0.67 0.67 ... ROOT 0.22file size 8975299 bytesLa disposición, sección por sección:
| Bytes | Contenido |
|---|---|
| 0 a 615 | la cabecera: número mágico, versión, hash del esquema, dimensión, recuento, un desplazamiento y un recuento por instrumento, cuatro desplazamientos de sección y luego 124 pesos float |
| 616 a 123.495 | un desplazamiento de 8 bytes por voicing hacia los metadatos: 15,360 × 8 = 122,880 bytes |
| 123.496 a 7.742.055 | los vectores: 124 floats por voicing, un instrumento tras otro |
| 7.742.056 hasta el final | los metadatos, un registro MessagePack por voicing: diagrama, instrumento, notas MIDI, nombre |
Los pesos de la cabecera son las raíces cuadradas de los pesos de las particiones: √0.45 = 0.67, √0.05 = 0.22. El marcador de orden de bytes 0xFEFF permite que un lector en una máquina big-endian note que los bytes están invertidos. El hash del esquema es el CRC-32 de la cadena de disposición de la lección 2; OptickIndexReader lanza InvalidDataException cuando no coincide con el suyo, así que un programa compilado con otra disposición no puede leer en silencio números equivocados. El resumen del escritor todavía dice que conserva “the 112 search-relevant dims” (línea 11); el archivo dice 124.
Un formato así es lo que diseñarías en C# para un array grande de solo lectura: registros de tamaño fijo a los que se llega con aritmética, y datos de tamaño variable detrás de una tabla de desplazamientos. Las particiones de información de la lección 2 no están en el archivo.
Volver a leerlo
Sección titulada «Volver a leerlo»OptickIndexReader no carga el archivo en memoria: lo proyecta en memoria (líneas 50-55) y entrega cada vector como un ReadOnlySpan<float> que apunta a las páginas proyectadas. El sistema operativo carga las páginas que se tocan, y varios procesos pueden compartirlas.
== OptickIndexReaderCount 15360guitar range (0, 15360)bass range (15360, 0)voicing 0 0-0-0-x-x-x Em/G [64 59 55]its squared norm 1.150max |disk - ExtractCompact| 1.5e-8La norma al cuadrado de cada vector almacenado es 1,15: cada partición tiene norma 1 multiplicada por √w, así que su contribución al cuadrado es w, y los pesos suman 1,15. La última línea compara los float almacenados con EmbeddingSchema.ExtractCompact calculado en double: coinciden hasta 1,5 × 10⁻⁸, la precisión de un float.
La proyección en memoria tiene un coste sobre el que avisa la skill de GA: en Windows, un proceso en ejecución que tiene el índice abierto bloquea el archivo, y una reconstrucción “WILL fail with IOException” mientras GaApi y el servidor MCP de GA no se detengan.
OptickSearchStrategy.SearchInternal es una búsqueda exacta de vecinos más cercanos, los k vecinos más cercanos por fuerza bruta del curso de IX, con un producto escalar en lugar de una distancia:
- un
TensorPrimitives.Dotpor voicing, acelerado con SIMD, sobre los vectores proyectados; - un
Parallel.Forsobre los voicings, en el que cada hilo mantiene su propiaPriorityQueuede loskmejores, fusionadas al final; - los empates se deshacen por índice, el menor primero, para que el resultado no dependa de la planificación de los hilos. El comentario explica por qué: muchos voicings comparten un conjunto de clases de altura y, por tanto, una puntuación.
Ni índice aproximado ni árbol: para el índice en producción, son 313.047 × 124, unos 39 millones de multiplicaciones por consulta.
El vector de consulta
Sección titulada «El vector de consulta»Un mensaje de chat no tiene mástil, así que MusicalQueryEncoder rellena solo lo que puede dar un cifrado de acorde: STRUCTURE a partir de las clases de altura, MODAL, SYMBOLIC cuando hay etiquetas y ROOT cuando el cifrado tiene fundamental. MORPHOLOGY y CONTEXT se quedan a cero, igual que el vector interválico dentro de STRUCTURE, “not yet wired into the query path” (líneas 52-58). La mejor puntuación posible queda, por tanto, por debajo de 0.45 + 0.10 + 0.05 = 0.60.
== HybridSearchAsync for "Cmaj7" (query vector from MusicalQueryEncoder)query partitions set: STRUCTURE, MODAL, ROOTrank diagram MIDI name score1 x-1-x-2-2-x 60 52 47 Cmaj7(shell)/B 0.4582 x-1-x-x-2-0 60 47 40 Cmaj7(shell)/E 0.4583 x-1-x-2-2-0 60 52 47 40 Cmaj7(shell)/E 0.4584 x-1-0-2-2-x 60 55 52 47 Cmaj7/B 0.4195 x-1-0-x-2-0 60 55 47 40 Cmaj7/E 0.419with the filter ChordName = "Cmaj7":rank diagram MIDI name score1 3-0-x-2-3-x 67 59 52 48 Cmaj7 0.3692 3-0-0-2-3-x 67 59 55 52 48 Cmaj7 0.369Todos los resultados sin filtrar tienen C, MIDI 60, como nota más aguda, y ninguno tiene C en el bajo: son inversiones y voicings reducidos (shells). Las dos formas de Cmaj7 en estado fundamental salen más abajo, con 0.369, porque su nota más aguda es G. La consulta dice “fundamental C”; el índice guardó la nota más aguda como fundamental (lección 2). Una búsqueda por cifrado de acorde premia las formas que tienen la fundamental arriba.
HybridSearchAsync añade filtros después de la búsqueda: toma max(10 × limit, 100) candidatos, y luego ApplyFilters separa "Cmaj7" en una fundamental, C, y una calidad, maj7, y conserva los candidatos cuyo nombre contiene maj7 y cuya nota más grave es un C. El filtro usa el bajo real; los vectores usaron la nota más aguda. Solo pasan dos formas, y son las dos que muestra el chatbot de la lección 4.
Am7 y C
Sección titulada «Am7 y C»== HybridSearchAsync for "Am7" (query vector from MusicalQueryEncoder)query partitions set: STRUCTURE, MODAL, ROOTrank diagram MIDI name score1 x-x-2-x-3-3 57 48 43 Am7(shell)/G 0.4582 x-x-2-2-3-x 57 52 48 Am/C 0.4503 x-x-2-x-3-0 57 48 40 Am/E 0.4504 x-x-2-2-3-0 57 52 48 40 Am/E 0.4505 x-x-2-2-3-3 57 52 48 43 Am7/G 0.419with the filter ChordName = "Am7":rank diagram MIDI name score1 2-x-x-2-0-x 66 52 45 Gbm7(shell)/A 0.3422 2-x-2-2-0-x 66 57 52 45 Gbm7(shell)/A 0.342
== HybridSearchAsync for "C" (query vector from MusicalQueryEncoder)query partitions set: STRUCTURE, MODAL, ROOTrank diagram MIDI name score1 x-1-0-2-x-x 60 55 52 C/E 0.4812 x-1-0-2-3-x 60 55 52 48 C 0.4813 x-1-0-x-x-0 60 55 40 C/E 0.4814 x-1-0-2-x-0 60 55 52 40 C/E 0.4815 x-1-0-x-3-0 60 55 48 40 C/E 0.481with the filter ChordName = "C":rank diagram MIDI name score1 x-1-0-2-3-x 60 55 52 48 C 0.4812 x-1-x-2-3-x 60 52 48 C + E (Major 3rd) 0.4533 x-1-0-x-3-x 60 55 48 C5 0.4154 x-1-2-2-3-x 60 57 52 48 Am/C 0.3935 x-1-x-0-3-x 60 50 48 C + D (Major 2nd) 0.367La prueba de calidad es una prueba de subcadena, así que el filtro Am7 acepta una forma llamada Gbm7(shell)/A: su nombre contiene m7 y su nota más grave es un A. Para "C", la calidad es la cadena vacía, que contienen todos los nombres: el filtro conserva cualquier cosa con C en el bajo, incluida una forma con solo C y E, y Am/C.
Los voicings más cercanos a un voicing
Sección titulada «Los voicings más cercanos a un voicing»== Nearest voicings to 0-1-0-2-3-x (open C), by its own compact vectorrank diagram MIDI name score1 0-1-0-2-3-x 64 60 55 52 48 C 1.1502 0-1-0-2-x-3 64 60 55 52 43 C/G 1.1503 0-1-0-x-3-x 64 60 55 48 C 1.1494 0-1-0-x-x-3 64 60 55 43 C/G 1.1495 0-1-x-2-x-3 64 60 52 43 C/G 1.1496 0-1-0-2-x-0 64 60 55 52 40 C/E 1.139FindSimilarVoicingsAsync("0-1-0-2-3-x") returned 0 resultsBuscar con un vector almacenado encuentra primero C abierto, y luego C/G con la misma puntuación hasta tres decimales: pasar el bajo de C a G, un cambio que cualquier guitarrista oye, casi no cuesta nada, porque el “bajo” del vector es la nota más aguda, E en los dos. El método pensado para esto, FindSimilarVoicingsAsync, devuelve una lista vacía para cualquier entrada: los metadatos OPTK “is keyed by index, not voicing id”, y registra un aviso una sola vez.
Dimensiones que no varían nunca
Sección titulada «Dimensiones que no varían nunca»Una dimensión que tiene el mismo valor para todos los voicings suma la misma cantidad a todas las puntuaciones: no puede cambiar un orden. El curso las cuenta sobre el corpus:
== Dimensions that never vary over the 15360 voicingspartition dims always 0 constant weightIDENTITY 6 3 3 0.00STRUCTURE 24 2 1 0.45MORPHOLOGY 24 5 0 0.25CONTEXT 12 11 1 0.20SYMBOLIC 12 3 2 0.10EXTENSIONS 18 6 1 0.00SPECTRAL 13 0 0 0.00MODAL 40 16 0 0.10HIERARCHY 15 14 0 0.00ATONAL_MODAL 64 47 0 0.00ROOT 12 0 0 0.05in the 124 compact dims: 37 always 0, 4 constantnamed MODAL slots never set (9 of 33): LocrianNatural6, DorianSharp4, LydianSharp2, AlteredDoubleFlat7, DorianFlat2, LydianAugmented, MixolydianFlat6, LocrianNatural2, DiminishedMODAL slots without a name constant: 7- CONTEXT, peso 0,20: 11 dimensiones siempre a cero y una constante, la tensión de la lección 2. La issue #616 midió lo mismo en el índice en producción, y 40 dimensiones muertas de 124; este corpus es pequeño, pero encuentra 41 que no varían nunca.
- ATONAL_MODAL: las 47 posiciones que el array de 17 elementos de la lección 2 no puede alcanzar.
- MODAL: 9 de las 33 posiciones con nombre no se activan nunca.
ModalVectorServicebusca cada modo por un nombre visible como"Locrian ♮6"o"Dorian ♯4", y vuelve sin escribir cuando la búsqueda no encuentra intervalos; un nombre que la búsqueda no conoce explicaría estas nueve (por verificar). Algunos ceros también pueden ser reales: un corpus de tres trastes tiene pocos voicings de los modos más raros.
Ejercicios
Sección titulada «Ejercicios»- La cabecera dice que los vectores empiezan en el byte 123.496 y los metadatos en el 7.742.056. Comprueba los dos números a partir del tamaño de la cabecera, el recuento y la dimensión.
- ¿Por qué la norma al cuadrado de cada vector almacenado es exactamente la suma de los pesos, y cuánto valdría para un voicing cuya partición SYMBOLIC es toda ceros?
- Un usuario pide al chatbot “C voicings”. Con los resultados filtrados para
"C", enumera lo que objetaría un guitarrista. - Escribe el bucle de productos escalares de
SearchInternalpara un solo hilo, sin el montículo: unforsobre los voicings que conserva la mejor puntuación y su índice.
Soluciones
-
La tabla de desplazamientos empieza justo después de la cabecera, en 616, con 8 bytes por voicing:
616 + 15,360 × 8 = 123,496. Los vectores ocupan15,360 × 124 × 4 = 7,618,560bytes:123,496 + 7,618,560 = 7,742,056. El archivo termina1,233,243bytes de metadatos más adelante, en 8.975.299, el tamaño impreso. -
ExtractCompactdivide cada partición por su norma y la multiplica por√w, así que la norma al cuadrado de cada partición esw, y las normas al cuadrado de partes disjuntas se suman. Una partición toda a ceros se queda a cero, así que la norma al cuadrado de ese voicing sería1.15 − 0.10 = 1.05. -
La forma
x-1-x-2-3-xsolo tiene dos clases de altura, C y E, y se llama “C + E (Major 3rd)”;x-1-0-x-3-xes un power chord C5, sin tercera;x-1-2-2-3-xes Am/C, otro acorde;x-1-x-0-3-xes C y D. Solo el primer resultado es un acorde de C mayor. El filtro comprueba el bajo y una subcadena del nombre, y la calidad vacía de"C"coincide con todos los nombres. -
Por ejemplo:
var best = (Score: float.NegativeInfinity, Index: -1L);for (long i = start; i < start + count; i++){var score = TensorPrimitives.Dot(q.AsSpan(), reader.GetVector(i));if (score > best.Score) best = (score, i); // estricto: el índice menor gana los empates}El
>estricto conserva el primer índice, el menor, entre puntuaciones iguales, el mismo desempate que los montículos de GA.
Puntos clave
Sección titulada «Puntos clave»- Un índice OPTK es un archivo binario plano: una cabecera con un hash del esquema, una tabla de desplazamientos, vectores
floatde tamaño fijo con las 124 dimensiones de similitud, y metadatos MessagePack. Se proyecta en memoria, no se carga. - La búsqueda es exacta: un producto escalar SIMD por voicing, montículos top-k por hilo y empates deshechos por índice para obtener resultados deterministas.
- El codificador de consultas solo puede rellenar STRUCTURE (sin su vector interválico), MODAL, SYMBOLIC y ROOT, así que las puntuaciones de un cifrado de acorde se quedan por debajo de 0,60.
- Como la fundamental almacenada es la nota más aguda mientras que el filtro usa el bajo, una búsqueda de “Cmaj7” pone primero las formas con C arriba, y el filtrado solo conserva dos formas del corpus pequeño; el filtro por nombre es una prueba de subcadena.
- Sobre 15.360 voicings, 41 de las 124 dimensiones buscadas no varían nunca, CONTEXT entre ellas: una parte de cada puntuación es una constante.
Fuentes
Sección titulada «Fuentes»- GuitarAlchemist/ga en
a826864:Demos/Music Theory/FretboardVoicingsCLI(OptickIndexWriter.cs,Program.cs),Common/GA.Business.ML/Search(OptickIndexReader.cs,OptickSearchStrategy.cs,MusicalQueryEncoder.cs),Common/GA.Business.ML/Embeddings/Services/ModalVectorService.cs,Common/GA.Domain.Services/Fretboard/Voicings/Generation/VoicingGenerator.cs,.claude/skills/optic-k-rebuild/SKILL.md. - Issue de GA #616, leída el 2026-09-14.
- Microsoft Learn: MemoryMappedFile, TensorPrimitives.Dot, PriorityQueue, Parallel.For, BinaryReader.
- Especificación de MessagePack.