cardmagic/classifier: Five Text Classifiers in One Ruby Gem
A general classifier module to allow Bayesian and LSI classifications.
At a glance
- What is it?
- A Ruby library that bundles Bayesian, logistic regression, LSI, k-NN and TF-IDF classification behind one gem, plus two command line tools. The incremental LSI path and the native extension are the parts worth understanding before you adopt it.
- Who is it for?
- Adopt cardmagic/classifier if you are writing Ruby and need a classifier you can train inside the same process that serves your application, or if you want the classifier and keywords commands in a shell pipeline. Do not adopt it if your text is not English-like enough for stemming to survive, if you need a model format that other runtimes can load, or if you need a probabilistic score you can threshold rather than a label.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What problem the classifier gem is built to solve
Most Ruby applications that need to sort text into buckets end up either shelling out to a Python service or hand-rolling a naive Bayes implementation. This gem is an attempt to remove both options. It ships four classifiers (Bayesian, logistic regression, LSI, k-nearest neighbors) plus TF-IDF vectorization in a single gem, with the README describing text classification in Ruby as the goal and listing five algorithms. The audience is a Ruby developer who has documents, labels, and a request path that cannot afford a network hop to another language runtime. The README's comparison table frames the project against unnamed forks of the same idea, claiming four classifiers plus TF-IDF where the others have two, plus command line executables the others lack. That table is the author's positioning, not an independent measurement, and it does not name the forks it is comparing against. Treat it as a statement of intent about scope rather than a benchmark.
How the four classifiers differ in mechanism
The algorithms are not interchangeable and the API differences reflect that. Bayes trains with a hash of label to text and classifies immediately, with no separate fit step: the README example creates Classifier::Bayes.new(:spam, :ham), calls train with strings or arrays of strings, and then calls classify. Logistic regression requires an explicit fit call before the first classify, which the README flags as required. That is the difference between a closed-form count model and one that needs an optimization pass over the training data. LSI uses add rather than train, taking a hash of label to document, and its classify returns the label whose documents are nearest in the reduced space. KNN is constructed with a k value and filled with add calls, one document at a time. TF-IDF has no labels at all: fit takes an array of strings and transform returns a hash of stem to weight, as in the example where transform on "Ruby programming" returns {rubi: 1.0}. Note the stem in that output. Stemming is applied throughout, and the CLI documentation states that the output maps stems back to whole words so a model built from programming prints programming rather than program.
Incremental LSI and the auto_rebuild switch
The feature the README spends the most space defending is incremental LSI. The claim is that adding documents does not require rebuilding the whole index, and that this uses Brand's algorithm. The other forks, per the table, do a full SVD rebuild on every add. The code path is explicit: construct Classifier::LSI.new(incremental: true, auto_rebuild: false), add the starting corpus, then build once. The README example adds five technology sentences in a single add call and then, in the truncated portion, presumably calls build. The point of the two flags is that auto_rebuild being on would trigger a rebuild after each add, which defeats the purpose. If you set incremental: true but leave auto_rebuild at its default, you get the slow path with extra bookkeeping. That is a configuration trap worth knowing about before you profile anything. The README also claims a native C extension makes LSI five to fifty times faster than pure Ruby or a GSL-dependent implementation. I have not run it, and the repository material does not include the benchmark that produced that range.
Running the classifier and keywords commands
The CLI is where this gem diverges most from other Ruby classifier libraries. Two executables ship. classifier handles labels: classifier -r sms-spam-filter "You won a free iPhone" prints spam, using a pre-trained model referenced by name. classifier train positive reviews/good/*.txt followed by classifier "Great product, highly recommend" trains and then classifies from files. keywords handles term importance and has no pre-trained models, so you must build a vocabulary first: keywords fit corpus/*.txt writes keywords.json and reports the path, or cat documents.txt | keywords fit reads from stdin. The fit step accepts filters, shown as keywords fit --min-df 2 --max-df 0.85 --ngram 1,2 corpus/*.txt. Scoring is keywords "Ruby is a programming language", which prints space-separated stem:weight pairs, and keywords extract article.txt for files, which the README shows piped from curl. keywords -n 5 limits the output, keywords -m custom_model.json points at a different model file, and keywords info prints document count, vocabulary size, min DF and max DF. The two commands keep separate model files in separate formats: classifier takes -f, keywords takes -m, and neither reads the other's file. A usage error exits 2, any other error exits 1, which is a deliberate choice for script authors.
Where the design breaks down
The stemming is the sharpest edge. Because the TF-IDF pipeline stems terms and the CLI maps stems back to whole words for display, the model is keyed on stems. The README's own example shows transform("Ruby programming") returning {rubi: 1.0}, which means the key is a stem, not the surface form. A lookup for the literal token ruby will not match. The README also warns that the terms printed alongside a label are context, not an explanation: TF-IDF measures how well a term separates a document from its corpus, not how much it favors a category. That is an honest caveat and it means you cannot present keywords output to a user as a reason for a classification. The second limitation is that classifier returns a label. The README's side-by-side example shows classifier -f reviews-model.json -p "Broken on arrival, awful quality" printing positive:0.12 negative:0.88, so probabilities are available with the -p flag, but the default output is a single word. If you need a calibrated threshold rather than an argmax, you are building that yourself. Third, the pre-trained models referenced by classifier -r are not enumerated in the material beyond three examples (sms-spam-filter, imdb-sentiment, emotion-detection), and the README only says classifier models lists them. How they were trained is not stated here.
Persistence, the native extension, and what to compare against
The README lists persistence as pluggable across file, Redis, S3, SQL and custom backends, and contrasts this with Marshal only. That is a list of intended adapters, not a demonstration; the material does not show the interface you implement for a custom backend, so budget time to read the source before committing to S3. The native C extension is the other deployment consideration: a C extension means your build environment needs a working toolchain, and precompiled binaries are not mentioned in the material. On the alternative side, the honest comparison is not another Ruby gem but scikit-learn in Python. scikit-learn gives you cross-validation, grid search, calibration, pipelines, and a model format that other services can load; this gem gives you an in-process Ruby object with no external service. The trade is real in both directions. If your classification runs inside a Rails request and the training set is a few thousand short documents, avoiding a Python sidecar is worth a lot. If you need to tune hyperparameters or explain individual predictions, you will feel the absence immediately.
Licence, releases and the cost of staying current
The README badge says LGPL 2.1, while the repository metadata reports the licence as NOASSERTION, which means GitHub could not identify a standard licence file. Those two signals disagree. LGPL 2.1 carries obligations around relinking and derivative works that a permissive licence does not, and I am not in a position to tell you what that means for your distribution model; read the actual LICENSE file and get your own advice. On maintenance, the release cadence shown is v2.7.0 in August 2026, v2.6.0 in June, and v2.5.0 in June, with the last push in September 2026 and the repository not archived. Three releases in roughly three months suggests active work, but it also means a minor version bump can land while you are mid-integration. The gem has no homepage of its own in the repository metadata; the documentation lives at rubyclassifier.com and in a docs directory. There is also a Claude Code plugin distributed through a separate marketplace repository, which is a distribution channel you would need to track separately from the gem.
Editorial conclusion
Adopt cardmagic/classifier if you are writing Ruby and need a classifier you can train inside the same process that serves your application, or if you want the classifier and keywords commands in a shell pipeline. Do not adopt it if your text is not English-like enough for stemming to survive, if you need a model format that other runtimes can load, or if you need a probabilistic score you can threshold rather than a label. Verify three things first: that the LGPL 2.1 label in the README matches the LICENSE file in the repository, that a native extension builds on your target platform, and that your persistence backend is actually implemented rather than only listed.
Community notes