Recherche instantanée de Rails et ActiveRecord à l'aide de vues matérialisées SQL.
Voir l'application de démonstration (code trouvé dans le dossier demo_app/
) :
Ajoutez des fonctionnalités de recherche rapide comme l'éclair à vos applications Rails sans systèmes externes comme ElasticSearch. Il est désormais magiquement simple de créer les expressions ActiveRecord/Arel que nous connaissons et aimons déjà, et de les convertir en vues matérialisées SQL : prêtes à être interrogées et composées avec ActiveRecord. Tout ce que vous aimez chez Rails, mais plus rapidement.
Qu’est-ce qui ralentit Rails pour la recherche ? Tables volumineuses, nombreuses jointures, sous-requêtes, index manquants ou inutilisés et requêtes complexes. Aussi lent ? Coordonner les données de plusieurs systèmes externes via Ruby pour produire des résultats de recherche.
SearchCraft simplifie l'écriture et l'utilisation de puissantes vues matérialisées SQL pour précalculer les résultats de vos requêtes de recherche et de reporting. C'est comme un index de base de données, mais pour les requêtes complexes.
Les vues matérialisées sont une fonctionnalité formidable de PostgreSQL, Oracle et SQL Server*. Il s'agit d'un tableau de résultats pré-calculés d'une requête. Ils sont rapides à interroger. Ils sont géniaux. Comme les autres systèmes de recherche, vous contrôlez quand vous souhaitez les actualiser avec de nouvelles données.
Dans Rails et ActiveRecord, vous pouvez accéder à une vue matérialisée en lecture seule comme vous le feriez pour n'importe quelle table ordinaire. Vous pouvez même les réunir. Vous pouvez les utiliser dans vos modèles, étendues et associations ActiveRecord.
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
end
Fait. Quelles que soient les colonnes que vous décrivez dans votre vue, elles deviendront des attributs de votre modèle.
Si la vue sous-jacente comportait les colonnes product_id
, product_name
, reviews_count
et reviews_average
, vous pouvez l'interroger comme n'importe quel autre modèle 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 vous incluez des clés étrangères, vous pouvez utiliser les associations belongs_to
. Vous pouvez ajouter des étendues. Vous pouvez ajouter des méthodes. Vous pouvez l'utiliser comme point de départ pour les requêtes avec le reste de votre base de données SQL. C'est juste un modèle ActiveRecord classique.
Tout cela est déjà possible avec Rails et ActiveRecord. La réussite de SearchCraft est de rendre trivial la vie avec vos vues matérialisées. Trivial de les rafraîchir et de les écrire.
Chaque SearchCraft affiche un instantané des résultats de la requête au moment de sa création ou de sa dernière actualisation. C'est comme une table dont le contenu est dérivé d'une requête.
Si les données sous-jacentes de votre vue matérialisée SearchCraft changent et que vous souhaitez les actualiser, appelez refresh!
sur votre classe modèle. Ceci est fourni par le mixin SearchCraft::Model
.
ProductSearch . refresh!
Vous pouvez transmettre cette relation/tableau ActiveRecord à vos vues Rails et les restituer. Vous pouvez le joindre à d'autres tables et appliquer d'autres étendues.
Mais la plus grande fonctionnalité de SearchCraft est de vous aider à rédiger vos vues matérialisées , puis à les parcourir.
Concevez-les dans des expressions ActiveRecord, des expressions Arel ou même du SQL simple. Aucune migration à annuler et à réexécuter. Il n'est pas possible de savoir si la vue SQL de votre base de données correspond au code SearchCraft de votre application Rails. SearchCraft créera et mettra automatiquement à jour vos vues matérialisées.
Mettez à jour votre vue SearchCraft, exécutez vos tests, ils fonctionnent. Mettez à jour votre vue SearchCraft, actualisez votre application de développement et cela fonctionne. Ouvrez rails console
et cela fonctionne ; puis mettez à jour votre vue, tapez reload!
, et ça marche. Déployez en production n’importe où et cela fonctionne.
À quoi ressemble la conception d’une vue matérialisée avec SearchCraft ? Pour notre modèle ProductSearch
ci-dessus, nous créons une classe ProductSearchBuilder
qui hérite de SearchCraft::Builder
et fournit soit une méthode view_scope
, soit une méthode 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
La méthode view_scope
doit renvoyer une relation ActiveRecord. Cela peut être aussi simple ou complexe que vous le souhaitez. Il peut utiliser des jointures, des sous-requêtes et tout ce que vous pouvez faire avec ActiveRecord. Dans l'exemple ci-dessus, nous :
id
et name
dans la table products
; où nous pourrons ensuite utiliser product_id
comme clé étrangère pour les jointures au modèle Product
dans notre applicationreviews_count
et reviews_average
à l'aide de sous-requêtes SQL qui comptent et font la moyenne de la colonne rating
de la table product_reviews
. SearchCraft le convertira en une vue matérialisée, le créera dans votre base de données et le modèle ProductSearch
ci-dessus commencera à l'utiliser lors du prochain rechargement de votre application de développement ou de l'exécution de vos tests. Si vous apportez une modification, SearchCraft supprimera et recréera automatiquement la vue.
Lorsque nous chargeons notre application dans la console Rails, exécutons nos tests ou actualisons l'application de développement, le modèle ProductSearch
sera automatiquement mis à jour pour correspondre à toutes les modifications apportées à 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 vous souhaitez écrire du SQL, vous pouvez utiliser la méthode view_select_sql
à la place.
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>]
Une fonctionnalité merveilleuse des vues matérialisées est que vous pouvez leur ajouter des index ; même des index uniques.
Actuellement, le mécanisme d'ajout d'index consiste à ajouter une méthode view_indexes
à votre classe de générateur.
Par exemple, nous pouvons ajouter un index unique sur la colonne number
de NumberBuilder
:
class NumberBuilder < SearchCraft :: Builder
def view_indexes
{
number : { columns : [ "number" ] , unique : true }
}
end
Ou plusieurs index sur ProductSearchBuilder
antérieurs :
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
Par défaut, les index using: :btree
. Vous pouvez également utiliser d'autres méthodes d'indexation disponibles dans Rails, telles que :gin
, :gist
, ou si vous utilisez l'extension trigram
vous pouvez utiliser :gin_trgm_ops
. Ceux-ci peuvent être utiles lorsque vous envisagez de configurer une recherche de texte, comme indiqué ci-dessous.
Un autre avantage des vues matérialisées est que nous pouvons créer des colonnes optimisées pour la recherche. Par exemple ci-dessus, puisque nous avons précalculé le reviews_average
dans ProductSearchBuilder
, nous pouvons facilement trouver des produits avec une certaine note moyenne.
ProductSearch . where ( "reviews_average > 4" )
Une fonctionnalité fabuleuse d'ActiveRecord est la possibilité de joindre des requêtes ensemble. Étant donné que nos vues matérialisées sont des modèles ActiveRecord natifs, nous pouvons les joindre à d’autres requêtes.
Configurons une association entre ProductSearch#product_id
de notre MV et la clé primaire de la table Product#id
:
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
belongs_to :product , foreign_key : :product_id , primary_key : :id
end
Nous pouvons maintenant joindre ou charger rapidement les tables avec les requêtes ActiveRecord. Ce qui suit renvoie une relation d'objets ProductSearch
, avec chacune de leurs associations ProductSearch#product
préchargées.
ProductSearch . includes ( :product ) . where ( "reviews_average > 4" )
Les éléments suivants renvoient des objets Product
, en fonction de la recherche dans la vue matérialisée 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 est livré avec une solution pour la recherche de texte utilisant une combinaison de fonctions telles que to_tsvector
, ts_rank
et websearch_to_tsquery
.
En attendant plus de documentation, consultez test/searchcraft/builder/test_text_search.rb
pour un exemple de la façon d'utiliser ces fonctions dans vos vues matérialisées.
Je travaille toujours sur l'extraction de cette solution de notre code sur Store Connect.
Une fois que vous disposez d'une vue matérialisée SearchCraft, vous souhaiterez peut-être en créer une autre qui en dépend. Vous pouvez également le faire avec la méthode 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 vous apportez une modification à NumberBuilder
, SearchCraft supprimera et recréera automatiquement les vues matérialisées Number
et 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>]
Vous n'êtes pas à l'aise pour écrire des expressions SQL ou Arel complexes ? Moi non plus. Je demande à GPT4 ou GitHub Copilot. J'explique la nature de mon schéma et de mes tables, et lui demande d'écrire du SQL, puis je demande de le convertir en Arel. Ou je lui donne un petit extrait de SQL et lui demande de le convertir en Arel. Je copie/colle ensuite les résultats dans ma classe de constructeur SearchCraft.
Cela vaut absolument la peine d'apprendre à exprimer vos requêtes de recherche en SQL ou Arel et à les placer dans une vue matérialisée SearchCraft. Vos utilisateurs vivront une expérience ultra-rapide.
Dans votre application Rails, ajoutez la gemme à votre Gemfile :
bundle add searchcraft
SearchCraft créera automatiquement une table de base de données interne dont il a besoin, il n'y aura donc aucune migration de base de données à exécuter. Et bien sûr, il créera et recréera automatiquement vos vues matérialisées.
Dans n'importe quelle application Rails, vous pouvez suivre ce didacticiel. Si vous n'avez pas d'application Rails, utilisez l'application trouvée dans le dossier demo_app
de ce projet.
Installez la gemme :
bundle add searchcraft
Choisissez l'un de vos modèles d'application existants, par exemple Product
, et nous créerons une vue matérialisée triviale pour celui-ci. Disons que nous voulons un moyen rapide d'obtenir les 5 produits les plus vendus et certains détails que nous utiliserons dans notre vue HTML.
Créez un nouveau fichier modèle ActiveRecord app/models/product_latest_arrival.rb
:
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Selon les conventions Rails, ce modèle recherchera une table ou une vue SQL appelée product_latest_arrivals
. Cela n'existe pas encore.
Nous pouvons le confirmer en ouvrant rails console
et en essayant de l'interroger :
ProductLatestArrival . all
# ActiveRecord::StatementInvalid ERROR: relation "product_latest_arrivals" does not exist
Nous pouvons créer une nouvelle classe de constructeur SearchCraft pour définir notre vue matérialisée. Créez un nouveau fichier app/searchcraft/product_latest_arrival_builder.rb
.
Je suggère app/searchcraft
pour vos constructeurs, mais ils peuvent accéder à n'importe quel app/
sous-dossier chargé automatiquement par Rails.
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product . order ( created_at : :desc ) . limit ( 5 )
end
end
Dans votre rails console``, run
reload! et vérifiez à nouveau votre requête :
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 la gem annotate
est installée dans votre Gemfile
, vous remarquerez également que le modèle product_latest_arrival.rb
a été mis à jour pour refléter les colonnes dans la vue matérialisée.
# == 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 votre application est sous contrôle de source, vous pouvez également voir que db/schema.rb
a été mis à jour pour refléter la dernière définition de vue. Exécutez 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
Vous pouvez maintenant continuer à modifier le view_scope
dans votre constructeur et exécuter reload!
dans la console Rails pour tester votre modification.
Par exemple, vous pouvez select()
uniquement les colonnes souhaitées en utilisant une expression SQL pour chacune d'entre elles :
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 vous pouvez utiliser des expressions Arel pour construire le 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'en est-il des mises à jour des données ? Créons plus Products
:
Product . create! ( name : "Starlink" )
Product . create! ( name : "Fishing Rod" )
Si vous deviez inspecter ProductLatestArrival.all
vous ne trouveriez pas ces nouveaux produits. En effet, la vue matérialisée est un instantané des données au moment de leur création ou de leur dernière actualisation.
Pour actualiser la vue :
ProductLatestArrival . refresh!
Alternativement, pour actualiser toutes les vues :
SearchCraft :: Model . refresh_all!
Et confirmez que les dernières nouveautés sont désormais dans la vue matérialisée :
ProductLatestArrival . pluck ( :name )
=> [ "Fishing Rod" , "Starlink" , "Sleek Steel Bag" , "Ergonomic Plastic Bench" , "Fantastic Wooden Keyboard" ]
Si vous souhaitez supprimer les artefacts de ce tutoriel. Tout d'abord, supprimez la vue matérialisée du schéma de votre base de données :
ProductLatestArrivalBuilder . new . drop_view!
Supprimez ensuite les fichiers et git checkout .
pour annuler toute autre modification.
rm app/searchcraft/product_latest_arrival_builder.rb
rm app/models/product_latest_arrival.rb
git checkout db/schema.rb
SearchCraft propose deux tâches de rake :
rake searchcraft:refresh
- actualise toutes les vues matérialiséesrake searchcraft:rebuild
- vérifie si des vues doivent être recréées Pour les ajouter à votre application Rails, ajoutez ce qui suit au bas de votre Rakefile
:
SearchCraft . load_tasks
Builder
et détecte automatiquement les modifications pour matérialiser le schéma de vue et le recréerefresh!
du contenu de la vue matérialiséedb/schema.rb
chaque fois que la vue matérialisée est mise à jourannotate
Gem est installérake searchcraft:refresh
et vérifier si des vues doivent être recréées rake searchcraft:rebuild
Après avoir extrait le dépôt, exécutez bin/setup
pour installer les dépendances. Ensuite, exécutez rake test
pour exécuter les tests. Vous pouvez également exécuter bin/console
pour une invite interactive qui vous permettra d'expérimenter.
Pour installer cette gemme sur votre machine locale, exécutez bundle exec rake install
. Pour publier une nouvelle version, mettez à jour le numéro de version dans version.rb
, puis exécutez bundle exec rake release
, qui créera une balise git pour la version, poussera les commits git et la balise créée, et poussera le fichier .gem
vers rubygems. org.
Pour modifier un numéro de version :
gem bump
, par exemple gem bump -v patch
demo_app/Gemfile.lock
, par exemple (cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
rake release
de libération gem bump -v patch
(cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
git push
rake release
Les rapports de bogues et les demandes d'extraction sont les bienvenus sur GitHub à l'adresse https://github.com/drnic/searchcraft. Ce projet se veut un espace de collaboration sûr et accueillant, et les contributeurs doivent adhérer au code de conduite.
La gemme est disponible en open source selon les termes de la licence MIT.
Toute personne interagissant dans les bases de code, les outils de suivi des problèmes, les salons de discussion et les listes de diffusion du projet Searchcraft doit respecter le code de conduite.
rails db:rollback
, reconstruction de la migration SQL, rails db:migrate
, puis test - est devenue lente. Cela a également introduit des bugs - j'oubliais d'exécuter les étapes, puis je constatais un comportement étrange. Si vous avez des vues relativement statiques ou des vues matérialisées et que vous souhaitez utiliser les migrations Rails, veuillez essayer scenic
Gem. Ce joyau searchcraft
dépend toujours de scenic
pour sa fonction refresh
de vue et l'ajout de vues dans schema.rb
.