Razmetka
Priority-dispatch sentence classifier with pluggable ML fallback for Russian NLP.
Part of the natasha-ex ecosystem. Extends yargy's defmatch bag-of-features matchers with ordered dispatch and ML classifier fallback.
- Grammar-first: bag-of-features rules checked in priority order
-
ML fallback: pluggable classifier (
Razmetka.Classifierbehaviour) for unmatched sentences - Zero coupling: works with FRIDA, fastText, or any embedding model
-
Reuses yargy's terminal predicates (
lemma,gram,token, etc.)
Installation
def deps do
[
{:razmetka, "~> 0.1"}
]
endUsage
defmodule MyApp.SentenceClassifier do
use Yargy.Grammar
use Razmetka
# Bag-of-features matchers (from yargy)
defmatch :demand, any_token(all([
lemma(~w[требовать просить взыскать]),
gram("VERB")
]))
defmatch :norm_framing, any_token(lemma(~w[соответствие согласно основание]))
defmatch :evidence, all_of([
any_token(lemma(~w[подтверждаться подтвердить])),
any_token(lemma(~w[акт квитанция чек выписка]))
])
# Priority dispatch + ML fallback
defclassify priority: [
{:demand, when: :demand},
{:norm, when: :norm_framing},
{:evidence, when: :evidence},
],
classifier: MyApp.FridaClassifier,
default: :fact,
threshold: 0.40
endClassify text
MyApp.SentenceClassifier.classify_text("Истец требует возмещения убытков")
#=> {:demand, %{confidence: :grammar}}
MyApp.SentenceClassifier.classify_text("Товар был поставлен 20 октября")
#=> {:fact, %{confidence: :classifier, score: 0.72}}Pre-tokenized input
tokens = Yargy.Pipeline.morph_tokenize("Истец требует возмещения")
MyApp.SentenceClassifier.classify(tokens, "Истец требует возмещения")
#=> {:demand, %{confidence: :grammar}}Pluggable classifiers
Implement the Razmetka.Classifier behaviour:
defmodule MyApp.FridaClassifier do
@behaviour Razmetka.Classifier
@impl true
def classify(text, _opts) do
clf = MyApp.ClassifierServer.get()
{type, score} = MyApp.NLP.Classifier.classify_one(clf, text)
{type, score}
end
end
The callback returns {type, score} where score is 0.0–1.0. Razmetka
compares against :threshold — below it, the :default type is used.
How it works
classify_text("В соответствии со ст. 309 ГК РФ...")
│
├─ tokenize + morph-tag (once, via yargy)
│
├─ Try :demand → no conjugated demand verb → skip
├─ Try :norm → has "соответствие" ✓ → MATCH
│ → {:norm, %{confidence: :grammar}}
│
└─ (never reaches :evidence or classifier)classify_text("Товар был поставлен 20 октября.")
│
├─ tokenize + morph-tag
│
├─ Try :demand → skip
├─ Try :norm → skip
├─ Try :evidence → skip
├─ Classifier fallback → FRIDA → {:fact, 0.72}
│ → {:fact, %{confidence: :classifier, score: 0.72}}Without classifier
defmodule MyApp.SimpleClassifier do
use Yargy.Grammar
use Razmetka
defmatch :greeting, any_token(lemma("привет"))
defclassify priority: [
{:greeting, when: :greeting},
],
default: :unknown
end
MyApp.SimpleClassifier.classify_text("Привет мир")
#=> {:greeting, %{confidence: :grammar}}
MyApp.SimpleClassifier.classify_text("Какой-то текст")
#=> {:unknown, %{confidence: :low}}License
MIT © Danila Poyarkov