Búsqueda instantánea de Rails y ActiveRecord utilizando vistas materializadas SQL.
Ver aplicación de demostración (el código se encuentra en la carpeta demo_app/
):
Agregue capacidades de búsqueda ultrarrápidas a sus aplicaciones Rails sin sistemas externos como ElasticSearch. Ahora es mágicamente sencillo crear las expresiones ActiveRecord/Arel que ya conocemos y amamos, y convertirlas en vistas materializadas SQL: listas para ser consultadas y compuestas con ActiveRecord. Todo lo que te encanta de Rails, pero más rápido.
¿Qué hace que Rails sea lento para la búsqueda? Tablas grandes, muchas combinaciones, subconsultas, índices faltantes o no utilizados y consultas complejas. ¿También lento? Coordinar datos de múltiples sistemas externos a través de Ruby para producir resultados de búsqueda.
SearchCraft hace que sea trivial escribir y utilizar potentes vistas materializadas de SQL para calcular previamente los resultados de sus consultas de búsqueda y generación de informes. Es como un índice de base de datos, pero para consultas complejas.
Las vistas materializadas son una característica maravillosa de PostgreSQL, Oracle y SQL Server*. Son una tabla de resultados precalculados de una consulta. Son rápidos para consultar. Son increíbles. Al igual que otros sistemas de búsqueda, usted controla cuándo desea actualizarlos con nuevos datos.
Dentro de Rails y ActiveRecord, puede acceder a una vista materializada de solo lectura como lo haría con cualquier tabla normal. Incluso puedes unirlos. Puede usarlos en sus modelos, alcances y asociaciones de ActiveRecord.
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
end
Hecho. Cualquier columna que describa en su vista se convertirá en atributos en su modelo.
Si la vista subyacente tenía columnas product_id
, product_name
, reviews_count
y reviews_average
, entonces puedes consultarla como cualquier otro modelo de 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>]
Si incluye claves externas, puede utilizar asociaciones belongs_to
. Puede agregar ámbitos. Puedes agregar métodos. Puede usarlo como punto de partida para consultas con el resto de su base de datos SQL. Es sólo un modelo ActiveRecord normal.
Todo esto ya es posible con Rails y ActiveRecord. El logro de SearchCraft es hacer que sea trivial vivir con sus vistas materializadas. Trivial para refrescarlos y escribirlos.
Cada SearchCraft materializa una vista instantánea de los resultados de la consulta en el momento en que se creó o se actualizó por última vez. Es como una tabla cuyo contenido se deriva de una consulta.
Si los datos subyacentes de su vista materializada de SearchCraft cambian y desea actualizarlos, ¡llame refresh!
en su clase modelo. Esto lo proporciona el mixin SearchCraft::Model
.
ProductSearch . refresh!
Puede pasar esta relación/matriz ActiveRecord a sus vistas de Rails y renderizarlas. Puede unirlo a otras tablas y aplicar más ámbitos.
Pero la característica más importante de SearchCraft es ayudarlo a escribir sus vistas materializadas y luego iterar sobre ellas.
Diseñelos en expresiones ActiveRecord, expresiones Arel o incluso SQL simple. No hay migraciones para revertir y volver a ejecutar. No es necesario realizar un seguimiento de si la vista SQL de su base de datos coincide con el código SearchCraft de su aplicación Rails. SearchCraft creará y actualizará automáticamente sus vistas materializadas.
Actualice su vista de SearchCraft, ejecute sus pruebas, funcionan. Actualice su vista SearchCraft, actualice su aplicación de desarrollo y funcionará. Abra rails console
y funciona; luego actualice su vista, escriba reload!
, y funciona. Implemente en producción en cualquier lugar y funcionará.
¿Cómo se ve diseñar una vista materializada con SearchCraft? Para nuestro modelo ProductSearch
anterior, creamos una clase ProductSearchBuilder
que hereda de SearchCraft::Builder
y proporciona un método view_scope
o un 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
El método view_scope
debe devolver una relación ActiveRecord. Puede ser tan simple o tan complejo como quieras. Puede usar uniones, subconsultas y cualquier otra cosa que puedas hacer con ActiveRecord. En el ejemplo anterior nosotros:
id
y name
de la tabla products
; donde luego podemos usar product_id
como clave externa para unirnos al modelo Product
en nuestra aplicaciónreviews_count
y reviews_average
utilizando subconsultas SQL que cuentan y promedian la columna rating
de la tabla product_reviews
. SearchCraft convertirá esto en una vista materializada, la creará en su base de datos y el modelo ProductSearch
anterior comenzará a usarlo la próxima vez que recargue su aplicación de desarrollo o ejecute sus pruebas. Si realiza un cambio, SearchCraft eliminará y recreará la vista automáticamente.
Cuando cargamos nuestra aplicación en la consola Rails, ejecutamos nuestras pruebas o actualizamos la aplicación de desarrollo, el modelo ProductSearch
se actualizará automáticamente para coincidir con cualquier cambio en 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>]
Si desea escribir SQL, puede utilizar el método view_select_sql
en su lugar.
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>]
Una característica maravillosa de las vistas materializadas es que puede agregarles índices; incluso índices únicos.
Actualmente, el mecanismo para agregar índices es agregar un método view_indexes
a su clase constructora.
Por ejemplo, podemos agregar un índice único en la columna number
de NumberBuilder
:
class NumberBuilder < SearchCraft :: Builder
def view_indexes
{
number : { columns : [ "number" ] , unique : true }
}
end
O varios índices en ProductSearchBuilder
de antes:
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
De forma predeterminada, los índices using: :btree
. También puedes usar otros métodos de indexación disponibles en Rails, como :gin
, :gist
, o si estás usando la extensión trigram
puedes usar :gin_trgm_ops
. Estos pueden resultar útiles a la hora de configurar la búsqueda de texto, como se explica a continuación.
Otro beneficio de las vistas materializadas es que podemos crear columnas optimizadas para la búsqueda. Por ejemplo, como hemos calculado previamente reviews_average
en ProductSearchBuilder
, podemos encontrar fácilmente productos con una determinada calificación promedio.
ProductSearch . where ( "reviews_average > 4" )
Una característica fabulosa de ActiveRecord es la capacidad de unir consultas. Dado que nuestras vistas materializadas son modelos nativos de ActiveRecord, podemos unirlas con otras consultas.
Configuremos una asociación entre ProductSearch#product_id
de nuestro MV y la clave primaria Product#id
de la tabla:
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
belongs_to :product , foreign_key : :product_id , primary_key : :id
end
Ahora podemos unir o cargar con entusiasmo las tablas junto con las consultas de ActiveRecord. A continuación se devuelve una relación de objetos ProductSearch
, con cada una de sus asociaciones ProductSearch#product
precargadas.
ProductSearch . includes ( :product ) . where ( "reviews_average > 4" )
Lo siguiente devuelve objetos Product
, según la búsqueda en la vista 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 viene con una solución para búsqueda de texto utilizando una combinación de funciones como to_tsvector
, ts_rank
y websearch_to_tsquery
.
A la espera de más documentos, consulte test/searchcraft/builder/test_text_search.rb
para ver un ejemplo de cómo utilizar estas funciones en sus vistas materializadas.
Todavía estoy trabajando para extraer esta solución de nuestro código en Store Connect.
Una vez que tenga una vista materializada de SearchCraft, es posible que desee crear otra que dependa de ella. También puedes hacer esto con el 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
Si realiza un cambio en NumberBuilder
, SearchCraft eliminará y recreará automáticamente las vistas materializadas Number
y 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>]
¿No se siente seguro al escribir expresiones SQL o Arel complejas? Yo tampoco. Le pregunto a GPT4 o GitHub Copilot. Explico la naturaleza de mi esquema y mis tablas, le pido que escriba algo de SQL y luego le pido que lo convierta a Arel. O le doy un pequeño fragmento de SQL y le pido que lo convierta a Arel. Luego copio/pego los resultados en mi clase de creación de SearchCraft.
Vale la pena aprender a expresar sus consultas de búsqueda en SQL o Arel y ponerlas en una vista materializada de SearchCraft. Sus usuarios tendrán una experiencia increíblemente rápida.
Dentro de tu aplicación Rails, agrega la gema a tu Gemfile:
bundle add searchcraft
SearchCraft creará automáticamente una tabla de base de datos interna que necesita, por lo que no es necesario ejecutar ninguna migración de base de datos. Y, por supuesto, creará y recreará automáticamente sus vistas materializadas.
Dentro de cualquier aplicación Rails puedes seguir este tutorial. Si no tiene una aplicación Rails, use la aplicación que se encuentra en la carpeta demo_app
de este proyecto.
Instale la gema:
bundle add searchcraft
Elija uno de sus modelos de aplicación existentes, digamos Product
, y crearemos una vista materializada trivial para él. Digamos que queremos una manera rápida de obtener los 5 productos más vendidos y algunos detalles que usaremos en nuestra vista HTML.
Cree un nuevo archivo de modelo ActiveRecord app/models/product_latest_arrival.rb
:
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Según las convenciones de Rails, este modelo buscará una tabla o vista SQL llamada product_latest_arrivals
. Esto aún no existe.
Podemos confirmar esto abriendo rails console
e intentando consultarla:
ProductLatestArrival . all
# ActiveRecord::StatementInvalid ERROR: relation "product_latest_arrivals" does not exist
Podemos crear una nueva clase de constructor SearchCraft para definir nuestra vista materializada. Cree un nuevo archivo app/searchcraft/product_latest_arrival_builder.rb
.
Sugiero app/searchcraft
para sus constructores, pero pueden ir a cualquier app/
subcarpeta que Rails cargue automáticamente.
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product . order ( created_at : :desc ) . limit ( 5 )
end
end
Dentro de tu rails console``, run
reload!` y revisa tu consulta nuevamente:
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" > ,
...
Si tiene la gema annotate
instalada en su Gemfile
, también notará que el modelo product_latest_arrival.rb
se ha actualizado para reflejar las columnas en la vista 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
Si su aplicación está bajo control de código fuente, también puede ver que db/schema.rb
se ha actualizado para reflejar la última definición de vista. Ejecute 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
¡Ahora puede continuar cambiando view_scope
en su constructor y ejecutar reload!
en la consola Rails para probar su cambio.
Por ejemplo, puede select()
solo las columnas que desee utilizando la expresión SQL para cada una:
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
O puedes usar expresiones de Arel para construir el 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
¿Qué pasa con las actualizaciones de datos? Creemos más Products
:
Product . create! ( name : "Starlink" )
Product . create! ( name : "Fishing Rod" )
Si inspeccionara ProductLatestArrival.all
no encontraría estos nuevos productos. Esto se debe a que la vista materializada es una instantánea de los datos en el momento en que se crearon o se actualizaron por última vez.
Para actualizar la vista:
ProductLatestArrival . refresh!
Como alternativa, para actualizar todas las vistas:
SearchCraft :: Model . refresh_all!
Y confirme que las últimas novedades ya están en la vista materializada:
ProductLatestArrival . pluck ( :name )
=> [ "Fishing Rod" , "Starlink" , "Sleek Steel Bag" , "Ergonomic Plastic Bench" , "Fantastic Wooden Keyboard" ]
Si desea eliminar los artefactos de este tutorial. Primero, elimine la vista materializada del esquema de su base de datos:
ProductLatestArrivalBuilder . new . drop_view!
Luego elimine los archivos y git checkout .
para revertir cualquier otro cambio.
rm app/searchcraft/product_latest_arrival_builder.rb
rm app/models/product_latest_arrival.rb
git checkout db/schema.rb
SearchCraft proporciona dos tareas de rake:
rake searchcraft:refresh
- actualiza todas las vistas materializadasrake searchcraft:rebuild
- comprueba si es necesario recrear alguna vista Para agregarlos a su aplicación Rails, agregue lo siguiente al final de su Rakefile
:
SearchCraft . load_tasks
Builder
y detecta automáticamente los cambios para materializar el esquema de vista y recrearlo.refresh!
de contenidos de vista materializadadb/schema.rb
cada vez que se actualiza la vista materializadaannotate
gemarake searchcraft:refresh
y verifique si es necesario recrear alguna vista rake searchcraft:rebuild
Después de revisar el repositorio, ejecute bin/setup
para instalar las dependencias. Luego, ejecute rake test
para ejecutar las pruebas. También puede ejecutar bin/console
para obtener un mensaje interactivo que le permitirá experimentar.
Para instalar esta joya en su máquina local, ejecute bundle exec rake install
. Para lanzar una nueva versión, actualice el número de versión en version.rb
y luego ejecute bundle exec rake release
, que creará una etiqueta git para la versión, enviará confirmaciones de git y la etiqueta creada, y enviará el archivo .gem
a rubygems. org.
Para aumentar un número de versión:
gem bump
, por ejemplo gem bump -v patch
demo_app/Gemfile.lock
, por ejemplo (cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
rake release
de liberación gem bump -v patch
(cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
git push
rake release
Los informes de errores y las solicitudes de extracción son bienvenidos en GitHub en https://github.com/drnic/searchcraft. Este proyecto pretende ser un espacio seguro y acogedor para la colaboración, y se espera que los contribuyentes respeten el código de conducta.
La gema está disponible como código abierto según los términos de la licencia MIT.
Se espera que todos los que interactúan en las bases de código, rastreadores de problemas, salas de chat y listas de correo del proyecto Searchcraft sigan el código de conducta.
rails db:rollback
, reconstruir SQL de migración, rails db:migrate
y luego probar) se volvió lento. También introdujo errores: me olvidaba de ejecutar los pasos y luego veía un comportamiento extraño. Si tiene vistas relativamente estáticas o vistas materializadas y desea utilizar migraciones de Rails, pruebe scenic
Gem. Esta joya searchcraft
todavía depende de scenic
para su función refresh
de vistas y para agregar vistas a schema.rb
.