User#followed_articles->Article.cached_tagged_with (#13174)

* User#followed_articles->Article.cached_tagged_with

This should improve performance on StoriesController#index

* Make cached_* work like their tagged_with cousins

* Swap to `cached_tagged_with_any`

This was using `tagged_with(tags, any: true)` before so this commit
swaps to using a scope that aligns with that method.

* Allow for searching by Tag models

* do the thing

* abracadabra run the build

* Add comment explaining Postgres regex anchors
This commit is contained in:
Jamie Gaskins 2021-04-01 16:20:29 -04:00 committed by GitHub
parent 18ce0c635c
commit 9533c77d7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 2 deletions

View file

@ -146,7 +146,37 @@ class Article < ApplicationRecord
.tagged_with(tag_name)
}
scope :cached_tagged_with, ->(tag) { where("cached_tag_list ~* ?", "^#{tag},| #{tag},|, #{tag}$|^#{tag}$") }
scope :cached_tagged_with, lambda { |tag|
case tag
when String
# In Postgres regexes, the [[:<:]] and [[:>:]] are equivalent to "start of
# word" and "end of word", respectively. They're similar to `\b` in Perl-
# compatible regexes (PCRE), but that matches at either end of a word.
# They're more comparable to how vim's `\<` and `\>` work.
where("cached_tag_list ~ ?", "[[:<:]]#{tag}[[:>:]]")
when Array
tag.reduce(self) { |acc, elem| acc.cached_tagged_with(elem) }
when Tag
cached_tagged_with(tag.name)
else
raise TypeError, "Cannot search tags for: #{tag.inspect}"
end
}
scope :cached_tagged_with_any, lambda { |tags|
case tags
when String
cached_tagged_with(tags)
when Array
tags
.map { |tag| cached_tagged_with(tag) }
.reduce { |acc, elem| acc.or(elem) }
when Tag
cached_tagged_with(tag.name)
else
raise TypeError, "Cannot search tags for: #{tag.inspect}"
end
}
scope :cached_tagged_by_approval_with, ->(tag) { cached_tagged_with(tag).where(approved: true) }

View file

@ -315,7 +315,7 @@ class User < ApplicationRecord
def followed_articles
Article
.tagged_with(cached_followed_tag_names, any: true).unscope(:select)
.cached_tagged_with_any(cached_followed_tag_names).unscope(:select)
.union(Article.where(user_id: cached_following_users_ids))
end

View file

@ -762,6 +762,89 @@ RSpec.describe Article, type: :model do
end
end
describe ".cached_tagged_with" do
it "can search for a single tag" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme").to_a
end
it "can search among multiple tags" do
included = [
create(:article, tags: "omg, wtf"),
create(:article, tags: "omg, lol"),
]
excluded = create(:article, tags: "nope, excluded")
articles = described_class.cached_tagged_with("omg")
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*described_class.tagged_with("omg").to_a)
end
it "can search for multiple tags" do
included = create(:article, tags: "includeme, please, lol")
excluded_partial_match = create(:article, tags: "excluded, please")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with(%w[includeme please])
expect(articles).to include included
expect(articles).not_to include excluded_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to eq described_class.tagged_with(%w[includeme please]).to_a
end
end
describe ".cached_tagged_with_any" do
it "can search for a single tag" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with_any("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme", any: true).to_a
end
it "can search among multiple tags" do
included = [
create(:article, tags: "omg, wtf"),
create(:article, tags: "omg, lol"),
]
excluded = create(:article, tags: "nope, excluded")
articles = described_class.cached_tagged_with_any("omg")
expected = described_class.tagged_with("omg", any: true).to_a
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags" do
included = create(:article, tags: "includeme, please, lol")
included_partial_match = create(:article, tags: "includeme, omg")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with_any(%w[includeme please])
expected = described_class.tagged_with(%w[includeme please], any: true).to_a
expect(articles).to include included
expect(articles).to include included_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to include(*expected)
end
end
context "when callbacks are triggered before save" do
it "assigns path on save" do
expect(article.path).to eq("/#{article.username}/#{article.slug}")