{"id":13878185,"url":"https://github.com/wbotelhos/rating","last_synced_at":"2026-04-27T18:05:50.229Z","repository":{"id":26455415,"uuid":"107933569","full_name":"wbotelhos/rating","owner":"wbotelhos","description":":star: A confidence-based rating system with scope and cache enabled","archived":false,"fork":false,"pushed_at":"2026-04-27T16:49:29.000Z","size":315,"stargazers_count":72,"open_issues_count":4,"forks_count":21,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-04-27T17:20:03.563Z","etag":null,"topics":["rate","rating","star-rating","vote"],"latest_commit_sha":null,"homepage":"https://wbotelhos.com","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wbotelhos.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"github":"wbotelhos"}},"created_at":"2017-10-23T04:51:42.000Z","updated_at":"2026-04-27T16:49:33.000Z","dependencies_parsed_at":"2024-01-31T05:10:11.786Z","dependency_job_id":"2a15a3c8-ab13-41b2-8fcf-16d7d8547b1f","html_url":"https://github.com/wbotelhos/rating","commit_stats":{"total_commits":163,"total_committers":3,"mean_commits":"54.333333333333336","dds":"0.024539877300613466","last_synced_commit":"51a3a9809f7835d201978109df0444e64d9df456"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/wbotelhos/rating","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Frating","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Frating/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Frating/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Frating/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wbotelhos","download_url":"https://codeload.github.com/wbotelhos/rating/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Frating/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32348058,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T17:12:42.749Z","status":"ssl_error","status_checked_at":"2026-04-27T17:12:41.658Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rate","rating","star-rating","vote"],"created_at":"2024-08-06T08:01:42.170Z","updated_at":"2026-04-27T18:05:50.223Z","avatar_url":"https://github.com/wbotelhos.png","language":"Ruby","readme":"# Rating\n\n[![Tests](https://github.com/wbotelhos/rating/workflows/Tests/badge.svg)](https://github.com/wbotelhos/rating/actions)\n[![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating)\n[![Maintainability](https://api.codeclimate.com/v1/badges/cc5efe8b06bc1d5e9e8a/maintainability)](https://codeclimate.com/github/wbotelhos/rating/maintainability)\n[![Coverage](https://codecov.io/gh/wbotelhos/rating/branch/master/graph/badge.svg?token=QJSHUOULEG)](https://codecov.io/gh/wbotelhos/rating)\n[![Sponsor](https://img.shields.io/badge/sponsor-%3C3-green)](https://github.com/sponsors/wbotelhos)\n\nA confidence-based rating system with scope and cache enabled.\n\n## JS Rating?\n\nThis is **Raty**: https://github.com/wbotelhos/raty :star2:\n\n## Description\n\nRating computes the `estimate` field using the **lower bound of the confidence interval** described\nby Evan Miller in [Ranking Items With Star Ratings](https://www.evanmiller.org/ranking-items-with-star-ratings.html).\n\nThe formula uses the full distribution of votes — not just the mean — so it considers both the\naverage rating and the confidence we have in that average:\n\n```\nestimate = mean − z × √(variance / (N + K + 1))\n```\n\nWhere:\n\n- `mean` and `variance` are computed over the smoothed vote distribution (Laplace prior of 1 vote per level);\n- `N` is the total number of votes for the resource;\n- `K` is the number of rating levels (e.g. 5 for a 1–5 star scale);\n- `z` is the normal distribution quantile (1.96 for 95% confidence).\n\nIn practice, this means:\n\n- An item with consistent votes (e.g. all 4-stars) ranks **above** an item with the same mean but polarized votes (half 1-stars, half 5-stars), because the latter has higher variance and thus more uncertainty;\n- An item with 3 five-star votes ranks **below** an item with 200 votes averaging 4.7 stars, because the term `√(variance / (N + K + 1))` shrinks as `N` grows.\n\nThis is the same family of approach used by sites like Amazon and IMDb to avoid items with very few votes dominating top-rated lists.\n\n### Configuration\n\nYou can tune the formula via `Rating::Config`:\n\n- `rating_levels` (default `5`): the maximum value of your rating scale. Vote `value` must be an integer between `1` and `rating_levels`. Set this in `config/rating.yml`:\n\n```yaml\nrating:\n  rating_levels: 10\n```\n\n- `rating_z_score` (default `1.96`): controls confidence level. Use `2.576` for 99% confidence (more conservative, penalizes low-vote items harder), `1.645` for 90% confidence (more permissive).\n\n## Install\n\nAdd the following code on your `Gemfile` and run `bundle install`:\n\n```ruby\ngem 'rating'\n```\n\nRun the following task to create the Rating migration:\n\n```sh\nrails g rating:install\n```\n\nThen execute the migrations to create the to create tables `rating_rates` and `rating_ratings`:\n\n```sh\nrake db:migrate\n```\n\n## Usage\n\nJust add the callback `rating` to your model:\n\n```ruby\nclass Author \u003c ApplicationRecord\n  rating\nend\n```\n\nNow this model can vote or receive votes.\n\n### rate\n\nYou can vote on some resource:\n\n```ruby\nauthor   = Author.last\nresource = Article.last\n\nauthor.rate resource, 3\n```\n\n### rating\n\nA voted resource exposes a cached data about it state:\n\n```ruby\nresource = Article.last\n\nresource.rating\n```\n\nIt will return a `Rating` object that keeps:\n\n`average`: the normal mean of votes;\n\n`estimate`: the lower bound (95% confidence) of the rating, based on the vote distribution. Use this for ranking — it penalizes both polarized distributions and items with few votes;\n\n`sum`: the sum of votes for this resource;\n\n`total`: the total of votes for this resource.\n\n### rate_for\n\nYou can retrieve the rate of some author gave to some resource:\n\n```ruby\nauthor   = Author.last\nresource = Article.last\n\nauthor.rate_for resource\n```\n\nIt will return a `Rate` object that keeps:\n\n`author`: the author of vote;\n\n`resource`: the resource that received the vote;\n\n`value`: the value of the vote.\n\n### rated?\n\nMaybe you want just to know if some author already rated some resource and receive `true` or `false`:\n\n```ruby\nauthor   = Author.last\nresource = Article.last\n\nauthor.rated? resource\n```\n\n### rates\n\nAll rating received.\n\n```ruby\nArticle.first.rates\n```\n\nIt will return a collection of `Rate` object.\n\n### rated\n\nAll rating given.\n\n```ruby\nAuthor.first.rated\n```\n\nIt will return a collection of `Rate` object.\n\n### order_by_rating\n\nYou can list resource ordered by rating data:\n\n```ruby\nArticle.order_by_rating\n```\n\nIt will return a collection of resource ordered by `estimate desc` as default.\nThe order column and direction can be changed:\n\n```ruby\nArticle.order_by_rating({ column: :average, direction: :asc })\n```\n\nIt will return a collection of resource ordered by `Rating` table data.\n\n### Scope\n\nAll methods support scope query, since you may want to vote on items of a resource instead the resource itself.\nLet's say an article belongs to one or more categories and you want to vote on some categories of this article.\n\n```ruby\ncategory_1 = Category.first\ncategory_2 = Category.second\nauthor     = Author.last\nresource   = Article.last\n```\n\nIn this situation you should scope the vote of article with some category:\n\n**rate**\n\n```ruby\nauthor.rate resource, 3, scope: category_1\nauthor.rate resource, 5, scope: category_2\n```\n\nNow `resource` has a rating for `category_1` and another one for `category_2`.\n\n**rating**\n\nRecovering the rating values for resource, we have:\n\n```ruby\nresource.rating\n# nil\n```\n\nBut using the scope to make the right query:\n\n```ruby\nresource.rating scope: category_1\n# { average: 3, estimate: 3, sum: 3, total: 1 }\n\nresource.rating scope: category_2\n# { average: 5, estimate: 5, sum: 5, total: 1 }\n```\n\n**rated**\n\nOn the same way you can find your rates with a scoped query:\n\n```ruby\nauthor.rated scope: category_1\n# { value: 3, scopeable: category_1 }\n```\n\n**rates**\n\nThe resource still have the power to consult its rates:\n\n```ruby\narticle.rates scope: category_1\n# { value: 3, scopeable: category_1 }\n\narticle.rates scope: category_2\n# { value: 3, scopeable: category_2 }\n```\n\n**order_by_rating**\n\nTo order the rating you do the same thing:\n\n```ruby\nArticle.order_by_rating({ scope: category_1 })\n```\n\n### Extra Scopes\n\nMaybe you need to use more than one scope to make a rate, so you can use the `extra_scopes` options.\nThis feature is enable **only** to restrict the rate, the rating calculation will **ignore** it.\n\nExample situation: I have a Profile (resource) that belongs to some Category (scope) and the Client (author) will rate\nthis Profile based on each Lead (extra scope) this Profile made. The Client can vote just one time on each lead, but many\ntimes to that Profile. The Profile has a rating score based on all leads made on that Category.\n\n```ruby\nscope    = Category.first\nauthor   = Client.last\nresource = Profile.last\nlead     = Lead.last\n\nauthor.rate resource, 5, extra_scopes: { lead_id: lead.id }, scope: scope\n```\n\n* The extra scopes fields is not present into gem, so you cannot use `{ lead: lead }`, for example.\n\nAll methods listed on [Scope](#scope) session allows `extra_scopes` as additional condition too.\n\n### Records\n\nMaybe you want to recover all records with or without scope, so you can add the suffix `_records` on relations:\n\n```ruby\ncategory_1 = Category.first\ncategory_2 = Category.second\nauthor     = Author.last\nresource   = Article.last\n\nauthor.rate resource, 1\nauthor.rate resource, 3, scope: category_1\nauthor.rate resource, 5, scope: category_2\n\nauthor.rating_records\n# { average: 1, estimate: 1, scopeable: nil       , sum: 1, total: 1 },\n# { average: 3, estimate: 3, scopeable: category_1, sum: 3, total: 1 },\n# { average: 5, estimate: 5, scopeable: category_2, sum: 5, total: 1 }\n\nauthor.rated_records\n# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }\n\narticle.rates_records\n# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }\n```\n\n### As\n\nIf you have a model that will only be able to rate but not to receive a rate, configure it as `author`.\nAn author model still can be rated, but won't generate a Rating record with all values as zero to warm up the cache.\n\n```ruby\nrating as: :author\n```\n\n### Metadata\n\nMaybe you want include a `comment` together your rating or even a `fingerprint` field to make your rating more secure.\nSo, first you will need to add more fields to the `Rating::Rate` table:\n\n```ruby\nclass AddCommentAndFingerprintOnRatingRates \u003c ActiveRecord::Migration\n  def change\n    add_column :rating_rates, :comment, :text\n\n    add_reference :rating_rates, :fingerprint, foreign_key: true, index: true, null: false\n  end\nend\n```\n\nAs you can seed, we can add any kind of field we want. Now we just provide this values when we make the rate:\n\n```ruby\nauthor      = Author.last\nresource    = Article.last\ncomment     = 'This is a very nice rating. s2'\nfingerprint = Fingerprint.new(ip: '127.0.0.1')\n\nauthor.rate resource, 3, metadata: { comment: comment, fingerprint: fingerprint }\n```\n\nNow you can have this data into your model normally:\n\n\n```ruby\nauthor = Author.last\nrate   = author.rates.last\n\nrate.comment     # 'This is a very nice rating. s2'\nrate.fingerprint # \u003cFingerprint id:...\u003e\nrate.value       # 3\n```\n\n### Scoping\n\nIf you need to warm up a record with scope, you need to setup the `scoping` relation.\n\n```ruby\nclass Resource \u003c ApplicationRecord\n  rating scoping: :categories\nend\n```\n\nNow, when a resource is created, the cache will be generated for each related `category` as `scopeable`.\n\n### Table Name\n\nYou can choose the table where Rating will write the data via YAML config.\nYou should just to provide a `config/rating.yml` file with the following content:\n\n```yml\nrating:\n  rate_table: reviews\n  rating_table: review_ratings\n```\n\nNow the rates will be written on `reviews` table over `rating_rates` and calculation will be on `review_ratings` over `rating_ratings`.\nYou can change one table o both of them.\n\n### Validations\n\n#### Rate Uniqueness\n\nSince you can to use [Extra Scopes](#extra_scopes) to restrict rates and the original model `Rating::Rate` is inside gem, you can configure the uniqueness validation, from outside, to include this extra scopes.\n\n```yml\nrating:\n  validations:\n    rate:\n      uniqueness:\n        case_sensitive: false\n\n        scope:\n          - author_type\n          - resource_id\n          - resource_type\n          - scopeable_id\n          - scopeable_type\n          - scope_1\n          - scope_2\n```\n\n### Unscoped Rating\n\nAll rating values are grouped by its own scope, but you can disable it and group all of them together.\n\n```ruby\nrating unscoped_rating: true\n\nauthor   = User.last\nresource = Article.last\nscope    = Category.last\n\nauthor.rate resource, 1, scope: scope\nauthor.rate resource, 2, scope: scope\nauthor.rate resource, 3\n```\n\nNow the `sum` will be `6` and the `total` will be `3` because all rating will be calculated into just one rating record ignoring the `scopeable` object.\nThe rating record is *always* saved on the record with `scopeable` as `nil`.\n\n### where\n\nThe `where` option can be used to filter the `Rating::Rate` records used to create the final `Rating::Rating`. You can filter only approved rates, for exemplo:\n\n```ruby\nrating where: 'approved = true'\n\nauthor   = User.last\nresource = Article.last\n\nauthor.rate resource, 1, extra_scope: { approved: false }\nauthor.rate resource, 5, extra_scope: { approved: true }\n```\n\nAs you can see, now, only the rate with value `5` will be included on the final rating.\n\n### Specs\n\nTo run all specs, run the following rakes:\n\n```sh\nbundle exec rake spec\nbundle exec rake spec_config\nbundle exec rake spec_config_with_extra_scopes\n```\n\n### References\n\n- [Evan Miller](http://www.evanmiller.org/ranking-items-with-star-ratings.html)\n","funding_links":["https://github.com/sponsors/wbotelhos"],"categories":["ORM/ODM Extensions","Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwbotelhos%2Frating","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwbotelhos%2Frating","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwbotelhos%2Frating/lists"}