Skip to main content
Uma extensão pode registrar sua própria estrutura de índice e fazer com que o servidor a chame. O servidor é dono da sintaxe SQL, dos metadados e da transação. A extensão é dona do formato em disco do índice e da busca dentro dele. Duas capabilities Preview sustentam isso. vsql::preview::index_type registra a estrutura de índice em si. vsql::preview::index_profile vincula essa estrutura a um tipo personalizado e nomeia as funções com as quais o índice calcula. Ambas ficam em <villagesql/preview/index_builder.h> nos cabeçalhos da dev ABI, então uma extensão que as usa é compilada com -DVSQL_USE_DEV_ABI=ON.
Suportado hoje: a DDL que cria, altera e remove um índice personalizado; a manutenção do índice em INSERT; e um formato de consulta, uma ordenação por vizinho mais próximo servida pelo índice.Não suportado: DELETE, um UPDATE que altera uma coluna indexada, e todo padrão de acesso que não seja a busca por vizinho mais próximo. Quatro dos doze hooks obrigatórios — mark_delete, purge, save e restore — precisam existir para que o builder compile, e o servidor nunca os chama.As instruções que nomeiam um índice personalizado são recusadas a menos que vsql_allow_preview_extensions esteja em ON, a mesma configuração que instalar uma extensão Preview já exige.

As Três Peças

Um tipo de índice é a estrutura: como as entradas são armazenadas, inseridas e percorridas. Ele é registrado uma vez por extensão e por estrutura, sob um nome como hnsw. Uma função de índice é uma função determinística vinculada a um perfil — uma função de distância, por exemplo. Um perfil vincula duas categorias de funções. Uma função vinculada com with_function() é uma para a qual o otimizador pode planejar um scan de índice. Uma função vinculada com with_helper() é chamada apenas pela implementação do índice. Vincular uma função a um perfil não cria uma função SQL. O otimizador casa uma chamada em ORDER BY pelo nome com as vinculações do perfil, então uma função vinculada com with_function() também deve ser registrada com .func() sob o mesmo nome, ou nenhuma instrução jamais poderá nomeá-la e o índice nunca será escolhido. Um helper não precisa de .func(), e chamar um a partir do SQL resulta em ERROR 1305 (42000): FUNCTION <database>.<name> does not exist. Um perfil de índice liga um tipo personalizado a um tipo de índice e dá a cada função vinculada um id numérico pelo qual o índice a chama. Um tipo de índice serve a muitos perfis: vsql_vector registra um único tipo de índice hnsw e quatro perfis sobre ele — hnsw_l1, hnsw_l2, hnsw_cosine e hnsw_inner_product — um por métrica de distância.

Registrando um Tipo de Índice

make_index_type<Name, Context>() recebe o nome do índice e uma struct de contexto por índice que guarda todo o estado de execução de que a extensão precisa. Todos os doze hooks são obrigatórios, em três grupos, nesta ordem. O builder não compila se um deles estiver faltando — a asserção que falha nomeia o hook. Os hooks de ciclo de vida criam o armazenamento do índice, o carregam quando a tabela é aberta e o removem:
Os hooks de DML mantêm o índice conforme as linhas mudam. O servidor chama insert. mark_delete e purge precisam existir com estas assinaturas para que o builder compile; implemente-os como stubs:
Os hooks de scan respondem às consultas. O servidor chama begin, position, fetch e end, e passa a position apenas Index::CursorOp::Next. save e restore precisam existir e são stubs, como os dois hooks de DML acima:
Todo hook que pode falhar recebe um buffer de mensagem e seu comprimento como os dois últimos argumentos, e retorna true em caso de falha depois de escrever a mensagem ali. O cursor pertence à extensão. Aloque-o em begin e atribua-o a *cursor, já posicionado no primeiro registro — o servidor chama fetch antes de chamar position. Libere o cursor em end e defina *cursor como nullptr. O SDK não encapsula o estado do cursor. O argumento bool * de begin, position e restore é uma flag de fim de scan que o hook escreve. Em caso de sucesso, o servidor chama end mesmo quando begin definiu essa flag; em caso de falha, begin deve deixar *cursor nulo, e end não é chamado. O MtrCtx::Ref que begin recebe é nulo no caminho que o servidor conduz hoje. O contexto não pertence à extensão. O SDK aloca Index::StorageCtx<T> e o seu T a partir da arena do InnoDB antes de chamar create ou load, e destrói a arena — o que executa ~T — depois que drop retorna. Alcance seu próprio estado por meio de ctx->user() e a arena por meio de ctx->arena(). Nunca exclua o contexto, o objeto de usuário ou a arena.

Declarando o Que o Índice Pode Fazer

.capabilities() declara quais padrões de acesso o índice suporta, e .storage_props() declara como cada entrada do índice se refere de volta aos dados que ela indexa. Ambos são obrigatórios. O builder aceita cinco valores de capability, e o servidor age sobre um: Index::Support::KNN, um scan dos k vizinhos mais próximos para ORDER BY <distance>(<column>, <reference>) LIMIT K. POINT_LOOKUP, RANGE_SCAN, REVERSE_SCAN e ORDER_BY compilam e são registrados, e o otimizador não constrói nenhum desses caminhos. Uma condição = na coluna indexada ainda pode planejar um Index lookup no índice personalizado, por exemplo sob FORCE INDEX, independentemente do que o índice declara, e essa consulta falha com ERROR 1031 (HY000): Table storage engine for '<table>' doesn't have this option. A declaração de armazenamento que serve a um scan de vizinho mais próximo é Index::Storage::HAS_COLUMN_REF | Index::Storage::HAS_ROW_REF em um índice de coluna única sobre uma coluna que a própria extensão armazena. HAS_COLUMN_REF significa que cada entrada armazena uma referência de coluna estável fornecida pelo servidor, em vez da chave primária completa, e é o que torna get_key_data() e get_key_ref() disponíveis. REF_LOOKUP significa que o índice consegue relocalizar uma entrada a partir dessa referência, e traz uma obrigação: toda entrada deve ter um token de referência único. Index::Storage::HAS_ROW_REF significa que cada entrada armazena a chave primária da linha que ela indexa: o servidor a passa para insert() no array pkeys, fetch() deve devolvê-la em pkeys, e o servidor resolve cada acerto com uma busca pela chave primária. Um índice que não o declara nunca é escolhido para um scan de vizinho mais próximo. Declare-o junto com HAS_COLUMN_REF.

Lendo e Escrevendo Referências de Coluna

get_key_data() resolve um KeyPartRef — do tipo que os hooks de DML e de scan entregam ao índice — no valor real da coluna de chave, dimensionado com get_max_col_len(key_pos). get_key_ref() faz o caminho inverso: deriva a referência para um valor de coluna de chave que já está no armazenamento de coluna, como um dos keys que insert() recebe. get_key_data() lê sob a sua própria mini-transação e não deve ser chamado enquanto a extensão mantém latches de página. Ambos recebem key_pos, a coluna de chave, com base zero, à qual o valor pertence, e retornam true em caso de falha, com a mensagem em index.get_error():
O servidor deixa ambos como pointers de função nulos a menos que o índice tenha declarado HAS_COLUMN_REF, e o SDK os chama sem proteção, então chamar qualquer um deles sem essa declaração causa um crash em vez de retornar um erro.

Opções de Índice

WITH (M = 8, ef_construction = 64) na DDL chega à extensão como opções de índice. O servidor converte cada chave e valor para minúsculas primeiro, então params[i].key vale m mesmo que a DDL tenha escrito M. Declare uma struct com valores padrão e uma função parse, e registre-a com .options<T, &T::parse>():
Compare com a chave em minúsculas:
params é nulo quando a DDL não trouxe uma cláusula WITH, então count é o valor a testar primeiro. Um parse que retorna falha aborta a DDL, e a mensagem que ele escreveu vai para o log de erros do servidor — o cliente vê apenas ERROR 1030 (HY000): Got error 168 - 'Unknown (generic) error from engine' from storage engine, então nomeie o índice e a chave problemática na mensagem. index.options<HNSWOptions>() retorna um pointer para a struct já parseada. Ele é alcançável a partir de todo hook que recebe o Index — os hooks de ciclo de vida, os hooks de DML e begin. Os demais hooks de scan recebem apenas um cursor, então um tipo de índice que precisa de suas opções durante o scan tem de copiá-las para o cursor em begin.

Funções de Índice

Uma função de índice deve ser determinística e deve retornar REAL. Construa uma e a mantenha em um static const — o perfil armazena uma referência a ela, portanto seu endereço tem de sobreviver ao registro:
Uma função pode declarar no máximo oito parâmetros, o valor de VEF_INDEX_PROFILE_FN_MAX_ARGS. O servidor verifica o tipo de retorno e a aridade no INSTALL EXTENSION, e não no primeiro uso.

Perfis de Índice

Um perfil nomeia o tipo personalizado, o tipo de índice e as funções com seus ids. for_type() recebe a string com o nome do tipo — a mesma constante passada para make_type — e não o descritor do tipo. with_function() e with_helper() recebem uma função de índice já construída, não um nome:
Os ids devem ser únicos dentro de cada lista, e as duas listas são numeradas de forma independente — with_function(1, ...) e with_helper(1, ...) não colidem. Um id repetido dentro de uma mesma lista é rejeitado no INSTALL EXTENSION. .default_for_type(true) marca este perfil como aquele a ser usado quando uma instrução não nomeia nenhum perfil. Aplica-se por par de tipo e tipo de índice, e tem o valor padrão false. O vsql_vector marca hnsw_l2, e é por isso que um índice criado sem um perfil registra hnsw_l2. .ordering() registra as direções de scan que o perfil suporta, como NONE, ASC, DESC, ou ASC | DESC. O servidor armazena o valor e nada o lê. Apenas uma ordenação ascendente é planejada, independentemente do que o perfil declara: um perfil que declara apenas DESC ainda serve a uma consulta ascendente, e um ORDER BY descendente é executado como um scan de tabela. Um hook chama uma função vinculada por meio de index.profile() ou um helper vinculado por meio de index.helper(). Ambos recebem a posição da coluna de chave, o id que o perfil deu à função, um pointer para o resultado e os valores das colunas como argumentos vef_storage_col_data_t. index.helper_fn_name() informa o nome registrado de um helper vinculado em vez de chamá-lo, o que permite que um laço interno resolva a sua própria implementação nativa uma única vez, em vez de despachar a cada chamada:

Juntando Tudo

Um esqueleto, sem os corpos dos hooks, a struct de opções e o contexto:
Ambos os objetos de capability devem ser static — o SDK mantém pointers para dentro deles — e cada um deve ser passado para .with() exatamente uma vez. Um único objeto de capability pode carregar várias estruturas: encadeie .index_type() ou .index_profile() por estrutura.

A Sintaxe SQL

Um índice personalizado é selecionado com USING EXTENDED, e a coluna pode nomear o perfil a ser usado:
A coluna indexada deve ser um tipo personalizado que alguma extensão instalada registra, e quando a DDL nomeia um perfil, esse perfil deve estar declarado para o tipo daquela coluna. Uma coluna comum é recusada com ERROR 3219 (HY000): No default index profile found: column '<name>' is not a custom type. Uma tabela particionada é recusada com ERROR 3219 (HY000): InnoDB: Custom index is not supported on partitioned tables. A cláusula funciona inline em CREATE TABLE, e em CREATE INDEX e ALTER TABLE ADD INDEX. Tanto o perfil quanto o tipo de índice podem ser qualificados com o nome da extensão, que é como você desambigua duas extensões que oferecem o mesmo nome. Quando nenhum perfil é nomeado, o servidor usa o perfil que declarou default_for_type(true) para aquele par de tipo e tipo de índice. Toda instrução desta seção precisa de vsql_allow_preview_extensions definido como ON, na inicialização ou com SET PERSIST. Sem isso, a instrução é recusada:
Usando o vsql_vector como exemplo, compile-o a partir do repositório vsql-vector, copie vsql_vector.veb para o veb_dir, ligue a configuração e instale a extensão primeiro:
Removê-lo é algo comum: DROP INDEX, ALTER TABLE ... DROP INDEX, ou DROP TABLE, que remove os metadados do índice junto com a tabela. Um índice personalizado e as linhas que ele contém sobrevivem a uma reinicialização do servidor.

Lendo e Escrevendo Através de um Índice Personalizado

As linhas entram com INSERT, e o índice é mantido conforme elas entram. Criar o índice em uma tabela que já contém linhas também funciona, e constrói o índice sobre elas. A construção bloqueia as escritas na tabela em vez de executar como uma DDL online; as leituras continuam. Um índice personalizado segue o caminho de construção offline com ALGORITHM=INPLACE, ALGORITHM=COPY ou sem cláusula, porque nada ainda reproduz escritas concorrentes dentro de um. ALGORITHM=INSTANT e LOCK=NONE são recusados. Um formato de consulta é servido por um índice personalizado: ORDER BY <distance>(<column>, <reference>) LIMIT K como o único termo de ordenação, ascendente, com um <reference> constante. O índice deve ser de coluna única, seu tipo deve declarar Index::Support::KNN e Index::Storage::HAS_ROW_REF, e seu perfil vinculado deve fornecer a função de distância. A coluna e a referência podem ser dadas em qualquer ordem. Uma consulta que não atende a qualquer uma dessas condições é executada como um scan de tabela e uma ordenação, sem nenhum aviso. O EXPLAIN nomeia o acesso:
O servidor não filtra os acertos do próprio índice pela visão da transação que lê. Ele resolve cada acerto para a sua linha, pula as linhas que essa visão não consegue ver e pega o próximo candidato — então begin deve devolver todo o seu conjunto de busca em vez de exatamente limit entradas, ou uma escrita concorrente deixa a consulta com menos de K linhas. Um UPDATE que altera uma coluna indexada é recusado:
Um UPDATE que altera a chave primária recebe o mesmo erro, assim como um REPLACE que atinge uma linha existente ou um INSERT ... ON DUPLICATE KEY UPDATE que atribui uma coluna indexada ou a chave primária. Um UPDATE ou um INSERT ... ON DUPLICATE KEY UPDATE que atribui apenas colunas não indexadas tem sucesso. Um ALTER TABLE que altera a chave primária de uma tabela com um índice personalizado é recusado independentemente do ALGORITHM:
Adicionar uma coluna, ou modificar uma coluna que não seja a chave primária, deixa o índice personalizado existente da tabela no lugar. Forçar uma reconstrução da tabela com ALGORITHM=COPY ou ALGORITHM=INPLACE também deixa o índice no lugar, e um índice reconstruído sobrevive a uma reinicialização.

O Que o Servidor Informa

SHOW CREATE TABLE mostra o perfil vinculado, o tipo de índice e quaisquer parâmetros WITH de um índice personalizado:
O perfil e o tipo de índice são sempre identificadores extension.name entre crases, nunca uma string. Tanto a chave quanto o valor de um parâmetro WITH são convertidos para minúsculas e ordenados alfabeticamente pela chave — a mesma normalização descrita em Opções de Índice acima — então uma chave escrita como M aparece como m. O perfil é sempre mostrado, mesmo quando a DDL nunca nomeou um e o servidor o resolveu para o padrão do tipo: os metadados persistidos registram apenas o nome resolvido. A renderização faz a ida e volta, então reproduzir uma string capturada de SHOW CREATE TABLE recria um índice que é mostrado de forma idêntica. INFORMATION_SCHEMA.STATISTICS.INDEX_TYPE e SHOW INDEX informam o nome do tipo de índice passado para USING EXTENDED(...), em maiúsculas, no lugar do nome do algoritmo do próprio mecanismo de armazenamento. O valor não carrega o qualificador da extensão, então duas extensões que oferecem o mesmo nome de tipo de índice são indistinguíveis nesta coluna. Um índice B-tree comum na mesma tabela continua informando BTREE:
Depois de DROP INDEX idx_a ON t2, a sua linha também desaparece desta consulta.

Onde Ficam os Metadados

O servidor registra cada índice personalizado em villagesql.custom_indexes, e suas colunas de chave — com o perfil vinculado a cada uma — em villagesql.custom_index_columns. Ambos sobrevivem a uma reinicialização. Nenhum dos dois é legível a partir do SQL, nem mesmo como root:
Trate-os como internos ao servidor. SHOW CREATE TABLE e INFORMATION_SCHEMA.STATISTICS são a forma suportada de inspecionar um índice personalizado a partir de um cliente.