Pesquisa instantânea por Rails e ActiveRecord usando visualizações materializadas SQL.
Veja o aplicativo de demonstração (código encontrado na pasta demo_app/
):
Adicione recursos de pesquisa extremamente rápida às suas aplicações Rails sem sistemas externos como o ElasticSearch. Agora é magicamente simples criar as expressões ActiveRecord/Arel que já conhecemos e amamos e convertê-las em visualizações SQL materializadas: prontas para serem consultadas e compostas com ActiveRecord. Tudo o que você adora no Rails, mas mais rápido.
O que torna o Rails lento para pesquisa? Tabelas grandes, muitas junções, subconsultas, índices ausentes ou não utilizados e consultas complexas. Também lento? Coordenar dados de vários sistemas externos por meio de Ruby para produzir resultados de pesquisa.
O SearchCraft torna trivial escrever e usar visualizações SQL materializadas poderosas para pré-calcular os resultados de suas consultas de pesquisa e relatórios. É como um índice de banco de dados, mas para consultas complexas.
Visualizações materializadas são um recurso maravilhoso do PostgreSQL, Oracle e SQL Server*. Eles são uma tabela de resultados pré-calculados de uma consulta. Eles são rápidos para consultar. Eles são incríveis. Como outros sistemas de pesquisa, você controla quando deseja atualizá-los com novos dados.
Dentro do Rails e do ActiveRecord, você pode acessar uma visualização materializada somente leitura, como faria com qualquer tabela normal. Você pode até juntá-los. Você pode usá-los em seus modelos, escopos e associações do ActiveRecord.
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
end
Feito. Quaisquer colunas que você descrever em sua visualização se tornarão atributos em seu modelo.
Se a visualização subjacente tiver colunas product_id
, product_name
, reviews_count
e reviews_average
, você poderá consultá-la como qualquer outro modelo ActiveRecord:
ProductSearch . all
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
ProductSearch . order ( reviews_average : :desc )
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
Se você incluir chaves estrangeiras, poderá usar associações belongs_to
. Você pode adicionar escopos. Você pode adicionar métodos. Você pode usá-lo como ponto de partida para consultas com o restante do seu banco de dados SQL. É apenas um modelo ActiveRecord normal.
Tudo isso já é possível com Rails e ActiveRecord. A conquista do SearchCraft é tornar trivial conviver com suas visões materializadas. Trivial atualizá-los e escrevê-los.
Cada SearchCraft materializado visualiza um instantâneo dos resultados da consulta no momento em que foi criada ou atualizada pela última vez. É como uma tabela cujo conteúdo deriva de uma consulta.
Se os dados subjacentes à sua visualização materializada do SearchCraft mudarem e você quiser atualizá-los, chame refresh!
na sua classe de modelo. Isso é fornecido pelo mixin SearchCraft::Model
.
ProductSearch . refresh!
Você pode passar esta relação/array ActiveRecord para suas visualizações Rails e renderizá-las. Você pode juntá-lo a outras tabelas e aplicar escopos adicionais.
Mas o maior recurso do SearchCraft é ajudá-lo a escrever suas visualizações materializadas e, em seguida, iterá-las.
Projete-os em expressões ActiveRecord, expressões Arel ou até mesmo em SQL simples. Não há migrações para reverter e executar novamente. Não é possível controlar se a visualização SQL em seu banco de dados corresponde ao código SearchCraft em seu aplicativo Rails. O SearchCraft criará e atualizará automaticamente suas visualizações materializadas.
Atualize sua visualização do SearchCraft, execute seus testes, eles funcionam. Atualize sua visualização do SearchCraft, atualize seu aplicativo de desenvolvimento e ele funcionará. Abra rails console
e ele funciona; em seguida, atualize sua visualização, digite reload!
, e funciona. Implante na produção em qualquer lugar e funciona.
Como é projetar uma visualização materializada com SearchCraft? Para nosso modelo ProductSearch
acima, criamos uma classe ProductSearchBuilder
que herda de SearchCraft::Builder
e fornece um método view_scope
ou um método view_select_sql
.
class ProductSearchBuilder < SearchCraft :: Builder
def view_scope
Product . where ( active : true )
. select (
"products.id AS product_id" ,
"products.name AS product_name" ,
"(SELECT COUNT(*) FROM product_reviews WHERE product_reviews.product_id = products.id) AS reviews_count" ,
"(SELECT AVG(rating) FROM product_reviews WHERE product_reviews.product_id = products.id) AS reviews_average"
)
end
end
O método view_scope
deve retornar uma relação ActiveRecord. Pode ser tão simples ou complexo quanto você quiser. Ele pode usar junções, subconsultas e qualquer outra coisa que você possa fazer com o ActiveRecord. No exemplo acima nós:
id
e name
na tabela products
; onde mais tarde podemos usar product_id
como chave estrangeira para junções ao modelo Product
em nosso aplicativoreviews_count
e reviews_average
usando subconsultas SQL que contam e calculam a média da coluna rating
da tabela product_reviews
. SearchCraft irá converter isso em uma visão materializada, criá-la em seu banco de dados, e o modelo ProductSearch
acima começará a usá-lo na próxima vez que você recarregar seu aplicativo de desenvolvimento ou executar seus testes. Se você fizer uma alteração, o SearchCraft irá descartar e recriar a visualização automaticamente.
Quando carregamos nosso aplicativo no console Rails, ou executamos nossos testes, ou atualizamos o aplicativo de desenvolvimento, o modelo ProductSearch
será atualizado automaticamente para corresponder a quaisquer alterações em ProductSearchBuilder
.
ProductSearch . all
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
ProductSearch . order ( reviews_average : :desc )
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
Se quiser escrever SQL, você pode usar o método view_select_sql
.
class NumberBuilder < SearchCraft :: Builder
# Write SQL that produces 5 rows, with a 'number' column containing the number of the row
def view_select_sql
"SELECT generate_series(1, 5) AS number;"
end
end
class Number < ActiveRecord :: Base
include SearchCraft :: Model
end
Number . all
[ #<Number number: 1>, #<Number number: 2>, #<Number number: 3>, #<Number number: 4>, #<Number number: 5>]
Um recurso maravilhoso das visualizações materializadas é que você pode adicionar índices a elas; até mesmo índices exclusivos.
Atualmente, o mecanismo para adicionar índices é adicionar um método view_indexes
à sua classe construtora.
Por exemplo, podemos adicionar um índice exclusivo na coluna number
de NumberBuilder
:
class NumberBuilder < SearchCraft :: Builder
def view_indexes
{
number : { columns : [ "number" ] , unique : true }
}
end
Ou vários índices anteriores no ProductSearchBuilder
:
class ProductSearchBuilder < SearchCraft :: Builder
def view_indexes
{
id : { columns : [ "product_id" ] , unique : true } ,
product_name : { columns : [ "product_name" ] } ,
reviews_count : { columns : [ "reviews_count" ] } ,
reviews_average : { columns : [ "reviews_average" ] }
}
end
end
Por padrão, os índices using: :btree
. Você também pode usar outros métodos de indexação disponíveis no Rails, como :gin
, :gist
, ou se estiver usando a extensão trigram
, você pode usar :gin_trgm_ops
. Eles podem ser úteis quando você deseja configurar a pesquisa de texto, conforme discutido abaixo.
Outro benefício das visualizações materializadas é que podemos criar colunas otimizadas para pesquisa. Por exemplo acima, como pré-calculámos o reviews_average
em ProductSearchBuilder
, podemos encontrar facilmente produtos com uma determinada classificação média.
ProductSearch . where ( "reviews_average > 4" )
Um recurso fabuloso do ActiveRecord é a capacidade de unir consultas. Como nossas visualizações materializadas são modelos ActiveRecord nativos, podemos juntá-las a outras consultas.
Vamos configurar uma associação entre ProductSearch#product_id
do nosso MV e a chave primária da tabela Product#id
:
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
belongs_to :product , foreign_key : :product_id , primary_key : :id
end
Agora podemos unir ou carregar antecipadamente as tabelas junto com as consultas do ActiveRecord. A seguir, retorna uma relação de objetos ProductSearch
, com cada uma de suas associações ProductSearch#product
pré-carregadas.
ProductSearch . includes ( :product ) . where ( "reviews_average > 4" )
O seguinte retorna objetos Product
, com base na pesquisa da visualização materializada ProductSearch
:
class Product
has_one :product_search , foreign_key : :product_id , primary_key : :id , class_name : "ProductSearch"
end
Product . joins ( :product_searches ) . merge (
ProductSearch . where ( "reviews_average > 4" )
)
PostgreSQL vem com uma solução para pesquisa de texto usando uma combinação de funções como to_tsvector
, ts_rank
e websearch_to_tsquery
.
Enquanto se aguarda mais documentos, consulte test/searchcraft/builder/test_text_search.rb
para obter um exemplo de como usar essas funções em suas visualizações materializadas.
Ainda estou trabalhando para extrair essa solução do nosso código no Store Connect.
Depois de ter uma visualização materializada do SearchCraft, você pode querer criar outra que dependa dela. Você também pode fazer isso com o método depends_on
.
class SquaredBuilder < SearchCraft :: Builder
depends_on "NumberBuilder"
def view_select_sql
"SELECT number, number * number AS squared FROM #{ Number . table_name } ;"
end
end
class Squared < ActiveRecord :: Base
include SearchCraft :: Model
end
Se você fizer uma alteração em NumberBuilder
, o SearchCraft descartará e recriará automaticamente as visualizações materializadas Number
e Squared
.
Squared . all
[ #<Squared number: 1, squared: 1>,
#<Squared number: 2, squared: 4>,
#<Squared number: 3, squared: 9>,
#<Squared number: 4, squared: 16>,
#<Squared number: 5, squared: 25>]
Não está confiante ao escrever expressões SQL ou Arel complexas? Eu também. Peço GPT4 ou GitHub Copilot. Explico a natureza do meu esquema e tabelas, peço para escrever um pouco de SQL e depois peço para convertê-lo em Arel. Ou dou a ele um pequeno trecho de SQL e peço para convertê-lo em Arel. Em seguida, copio/colo os resultados em minha classe do construtor SearchCraft.
Vale absolutamente a pena aprender a expressar suas consultas de pesquisa em SQL ou Arel e colocá-las em uma visualização materializada do SearchCraft. Seus usuários terão uma experiência extremamente rápida.
Dentro do seu aplicativo Rails, adicione a gem ao seu Gemfile:
bundle add searchcraft
O SearchCraft criará automaticamente uma tabela de banco de dados interna necessária, portanto, não há migração de banco de dados para executar. E, claro, ele criará e recriará automaticamente suas visualizações materializadas.
Dentro de qualquer aplicativo Rails você pode acompanhar este tutorial. Se você não possui um aplicativo Rails, use o aplicativo encontrado na pasta demo_app
deste projeto.
Instale a joia:
bundle add searchcraft
Escolha um dos seus modelos de aplicação existentes, digamos Product
, e criaremos uma visão materializada trivial para ele. Digamos que queremos uma maneira rápida de obter os 5 produtos mais vendidos e alguns detalhes que usaremos em nossa visualização HTML.
Crie um novo arquivo de modelo ActiveRecord app/models/product_latest_arrival.rb
:
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Pelas convenções do Rails, este modelo procurará uma tabela ou visualização SQL chamada product_latest_arrivals
. Isto ainda não existe.
Podemos confirmar isso abrindo rails console
e tentando consultá-lo:
ProductLatestArrival . all
# ActiveRecord::StatementInvalid ERROR: relation "product_latest_arrivals" does not exist
Podemos criar uma nova classe construtora SearchCraft para definir nossa visão materializada. Crie um novo arquivo app/searchcraft/product_latest_arrival_builder.rb
.
Eu sugiro app/searchcraft
para seus construtores, mas eles podem ir para qualquer app/
subpasta que seja carregada automaticamente pelo Rails.
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product . order ( created_at : :desc ) . limit ( 5 )
end
end
Dentro do seu rails console``, run
reload!` e verifique sua consulta novamente:
reload!
ProductLatestArrival . all
ProductLatestArrival Load ( 1.3 ms ) SELECT "product_latest_arrivals" . * FROM "product_latest_arrivals"
=>
[ #<ProductLatestArrival:0x000000010a737d18
id : 1 ,
name : "Rustic Wool Coat" ,
active : true ,
created_at : Fri , 25 Aug 2023 07 :15 :16 . 995228000 UTC + 00 : 00 ,
updated_at : Fri , 25 Aug 2023 07 :15 :16 . 995228000 UTC + 00 : 00 ,
image_url : "https://loremflickr.com/g/320/320/coat?lock=1" > ,
...
Se você tiver o annotate
gem instalado em seu Gemfile
, também notará que o modelo product_latest_arrival.rb
foi atualizado para refletir as colunas na visualização materializada.
# == Schema Information
#
# Table name: product_latest_arrivals
#
# id :bigint
# name :string
# active :boolean
# created_at :datetime
# updated_at :datetime
# image_url :string
#
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Se seu aplicativo estiver sob controle de origem, você também poderá ver que db/schema.rb
foi atualizado para refletir a definição de visualização mais recente. Execute git diff db/schema.rb
:
create_view "product_latest_arrivals" , materialized : true , sql_definition : <<-SQL
SELECT products.id,
products.name,
products.active,
products.created_at,
products.updated_at,
products.image_url
FROM products
LIMIT 5;
SQL
Agora você pode continuar alterando o view_scope
em seu construtor e executar reload!
no console do Rails para testar sua alteração.
Por exemplo, você pode select()
apenas as colunas que deseja usando a expressão SQL para cada uma:
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product
. order ( created_at : :desc )
. limit ( 5 )
. select (
"products.id as product_id" ,
"products.name as product_name" ,
"products.image_url as product_image_url" ,
)
end
end
Ou você pode usar expressões Arel para construir o SQL:
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product
. order ( created_at : :desc )
. limit ( 5 )
. select (
Product . arel_table [ :id ] . as ( "product_id" ) ,
Product . arel_table [ :name ] . as ( "product_name" ) ,
Product . arel_table [ :image_url ] . as ( "product_image_url" ) ,
)
end
end
E quanto às atualizações de dados? Vamos criar mais Products
:
Product . create! ( name : "Starlink" )
Product . create! ( name : "Fishing Rod" )
Se você inspecionasse ProductLatestArrival.all
não encontraria esses novos produtos. Isso ocorre porque a visualização materializada é um instantâneo dos dados no momento em que foram criados ou atualizados pela última vez.
Para atualizar a visualização:
ProductLatestArrival . refresh!
Como alternativa, para atualizar todas as visualizações:
SearchCraft :: Model . refresh_all!
E confirme se as últimas novidades estão agora na visualização materializada:
ProductLatestArrival . pluck ( :name )
=> [ "Fishing Rod" , "Starlink" , "Sleek Steel Bag" , "Ergonomic Plastic Bench" , "Fantastic Wooden Keyboard" ]
Se você deseja remover os artefatos deste tutorial. Primeiro, elimine a visualização materializada do esquema do seu banco de dados:
ProductLatestArrivalBuilder . new . drop_view!
Em seguida, remova os arquivos e git checkout .
para reverter quaisquer outras alterações.
rm app/searchcraft/product_latest_arrival_builder.rb
rm app/models/product_latest_arrival.rb
git checkout db/schema.rb
SearchCraft oferece duas tarefas de rake:
rake searchcraft:refresh
- atualiza todas as visualizações materializadasrake searchcraft:rebuild
- verifique se alguma visualização precisa ser recriada Para adicioná-los ao seu aplicativo Rails, adicione o seguinte na parte inferior do seu Rakefile
:
SearchCraft . load_tasks
Builder
e detecta automaticamente alterações para materializar o esquema de visualização e recria-orefresh!
do conteúdo da visualização materializadadb/schema.rb
sempre que a visualização materializada é atualizadaannotate
estiver instaladarake searchcraft:refresh
e verifique se alguma visualização precisa ser recriada rake searchcraft:rebuild
Depois de verificar o repositório, execute bin/setup
para instalar as dependências. Em seguida, execute rake test
para executar os testes. Você também pode executar bin/console
para obter um prompt interativo que permitirá experimentar.
Para instalar esta jóia em sua máquina local, execute bundle exec rake install
. Para lançar uma nova versão, atualize o número da versão em version.rb
e, em seguida, execute bundle exec rake release
, que criará uma tag git para a versão, enviará git commits e a tag criada e enviará o arquivo .gem
para rubygems. organização.
Para aumentar um número de versão:
gem bump
, por exemplo, gem bump -v patch
demo_app/Gemfile.lock
, por exemplo (cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
rake release
gem bump -v patch
(cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
git push
rake release
Relatórios de bugs e solicitações pull são bem-vindos no GitHub em https://github.com/drnic/searchcraft. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e espera-se que os colaboradores cumpram o código de conduta.
A gema está disponível como código aberto sob os termos da licença MIT.
Espera-se que todos que interagem nas bases de código, rastreadores de problemas, salas de bate-papo e listas de discussão do projeto Searchcraft sigam o código de conduta.
rails db:rollback
, reconstruir SQL de migração, rails db:migrate
e, em seguida, test - tornou-se lenta. Ele também introduziu bugs - eu esquecia de executar as etapas e via um comportamento estranho. Se você tem visualizações relativamente estáticas ou visualizações materializadas e deseja usar migrações Rails, experimente scenic
Gem. Esta joia searchcraft
ainda depende do scenic
para seu recurso refresh
de visualização e da adição de visualizações em schema.rb
.