summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorunwox <me@unwox.com>2024-10-15 14:48:16 +0600
committerunwox <me@unwox.com>2024-10-15 14:49:36 +0600
commit1204496efa2fcd495bd74ba8ca249b7f082f3ba5 (patch)
tree57f908c7bae9ff1194ccb68328ec59ef5be00872
parent1520d83acccdcad2d7f87aec073b48d5f4995bf6 (diff)
WIP try to fix spelling mistakes in search querie
currently works very slowly and uses a lot of CPU
-rw-r--r--bin/serve.fnl237
-rw-r--r--lib/array.fnl6
-rw-r--r--lib/string.fnl29
-rw-r--r--spellfix.fnl336
-rw-r--r--texts.fnl2
5 files changed, 519 insertions, 91 deletions
diff --git a/bin/serve.fnl b/bin/serve.fnl
index 4d982c2..4cbb905 100644
--- a/bin/serve.fnl
+++ b/bin/serve.fnl
@@ -9,6 +9,9 @@
(local json (require :vendor.json))
(local array (require :lib.array))
(local str (require :lib.string))
+(local texts (require :texts))
+(local spellfix (require :spellfix))
+(local {: must} (require :lib.utils))
(local ozchai (require :parser.ozchai))
(local ipuer (require :parser.ipuer))
@@ -17,7 +20,7 @@
(when _G.unpack
(tset table :unpack _G.unpack))
-(local db (luna.db.open "file:var/db.sqlite?_journal=WAL&_sync=NORMAL"))
+(local db (must (luna.db.open "file:var/db.sqlite?_journal=WAL&_sync=NORMAL")))
(local query-synonyms {
"шэн" "шен"
@@ -36,9 +39,66 @@
(string.gsub "&quot;" "\"")
(string.gsub "&amp;" "&"))))
+(fn get-query-string [query key]
+ (if (and query
+ (. query key)
+ (. query key 1)
+ (< 0 (# (. query key 1))))
+ (. query key 1)
+ nil))
+
+(fn get-query-number [query key]
+ (if (and query
+ (. query key)
+ (. query key 1)
+ (< 0 (# (. query key 1))))
+ (tonumber (. query key 1))
+ nil))
+
+(fn collect-form [params]
+ {:query (str.trim (or (get-query-string params "query") ""))
+ :tags (filter #(~= "" $2) (or params.tags []))
+ :min-price (get-query-number params "min-price")
+ :max-price (get-query-number params "max-price")
+ :price-per (= "on" (get-query-string params "price-per"))})
+
+(fn form-empty? [form]
+ (and
+ (= "" form.query)
+ (= (# form.tags) 0)
+ (not form.min-price)
+ (not form.max-price)
+ ;; price-per is intentionally left out since it must not trigger search
+ ;; by itself
+ ))
+
+(fn form->path [page form]
+ (.. "?page=" (tostring page)
+ (if (not (str.empty? form.query))
+ (.. "&query=" form.query)
+ "")
+ (if (< 0 (# form.tags))
+ (.. "&"
+ (array.join
+ (array.flatten
+ (map (fn [_ tag] (.. "tags=" tag))
+ form.tags))
+ "&"))
+ "")
+ (if (and form.min-price (< 0 form.min-price))
+ (.. "&min-price=" (tostring form.min-price))
+ "")
+ (if (and form.max-price (< 0 form.max-price))
+ (.. "&max-price=" (tostring form.max-price))
+ "")
+ (if form.price-per
+ "&price-per=on"
+ "")))
+
(fn random-products [limit]
(assert (< 0 limit) "limit must be > 0")
- (luna.db.query*
+ (must
+ (luna.db.query*
db
"SELECT site,
title,
@@ -52,17 +112,18 @@
FROM products
ORDER BY RANDOM()
LIMIT ?"
- [limit]))
+ [limit])))
(fn all-tags []
(map
(fn [_ v] (. v 1))
- (luna.db.query
+ (must
+ (luna.db.query
db
"SELECT title FROM tags ORDER BY creation_time"
- [])))
+ []))))
-(fn query-products [page query tags]
+(fn query-products [{: query : tags : min-price : max-price : price-per} page]
(local tags (or tags []))
(var where-conds [])
@@ -81,42 +142,59 @@
(.. q "*")))
(str.split query))
" AND ")))
- (local where-sql (array.join where-conds "\nAND "))
+ (when (and min-price (< 0 min-price))
+ (if price-per
+ (table.insert where-conds "products.price_per >= ?")
+ (table.insert where-conds "products.price >= ?"))
+ (table.insert where-vars min-price))
+ (when (and max-price (< 0 max-price))
+ (if price-per
+ (table.insert where-conds "products.price_per <= ?")
+ (table.insert where-conds "products.price <= ?"))
+ (table.insert where-vars max-price))
+
+ (local where-sql
+ (if (< 0 (# where-conds))
+ (.. "AND " (array.join where-conds "\nAND "))
+ ""))
(local total
- (luna.db.query
- db
- (string.format
- "SELECT count(*)
- FROM search
- LEFT JOIN product_tags ON product_tags.product = search.fid
- WHERE search.`table` = 'products'
- AND %s" where-sql)
- where-vars))
+ (must
+ (luna.db.query
+ db
+ (string.format
+ "SELECT count(*)
+ FROM search
+ INNER JOIN products ON search.fid = products.url
+ LEFT JOIN product_tags ON product_tags.product = search.fid
+ WHERE search.`table` = 'products'
+ %s" where-sql)
+ where-vars)))
{:results
+ (must
(luna.db.query*
- db
- (string.format
- "SELECT highlight(search, 0, '<i>', '</i>') AS \"title\",
- products.site,
- products.description,
- products.image,
- products.url,
- products.price,
- products.weight,
- products.price_per AS \"price-per\",
- products.year,
- products.archived,
- products.creation_time AS \"creation-time\"
- FROM search
- INNER JOIN products ON search.fid = products.url
- LEFT JOIN product_tags ON product_tags.product = products.url
- WHERE search.`table` = 'products'
- AND %s
- ORDER BY rank
- LIMIT 48 OFFSET ?" where-sql)
- (array.concat where-vars [(* (- page 1) 48)]))
+ db
+ (string.format
+ "SELECT highlight(search, 0, '<i>', '</i>') AS \"title\",
+ products.site,
+ products.description,
+ products.image,
+ products.url,
+ products.price,
+ products.weight,
+ products.price_per AS \"price-per\",
+ products.year,
+ products.archived,
+ products.creation_time AS \"creation-time\"
+ FROM search
+ INNER JOIN products ON search.fid = products.url
+ LEFT JOIN product_tags ON product_tags.product = products.url
+ WHERE search.`table` = 'products'
+ %s
+ ORDER BY rank
+ LIMIT 48 OFFSET ?" where-sql)
+ (array.concat where-vars [(* (- page 1) 48)])))
:total (if (< 0 (# total))
(. total 1 1)
0)})
@@ -170,48 +248,39 @@
;; FIXME: security issue
[:small {} [:NO-ESCAPE (unescape (str.truncate product.description 200))]]])
-(fn paginator-template [query tags page limit total]
+(fn paginator-template [form page limit total]
(local last-page (math.ceil (/ total limit)))
- (fn make-url [page query tags]
- (.. "?page=" (tostring page)
- "&query=" query
- "&"
- (array.join
- (array.flatten
- (map (fn [_ tag] (.. "tags=" tag))
- tags))
- "&")))
(if (< limit total)
[:div {:class "paginator"}
[:div {:class "paginator-numbers"}
(if (< 1 page)
- [:a {:href (make-url (- page 1) query tags)} "←"]
+ [:a {:href (form->path (- page 1) form)} "←"]
"")
(faccumulate [res [:span {}] i 1 last-page]
(do
(table.insert
- res [:a {:href (make-url i query tags)
+ res [:a {:href (form->path i form)
:class (if (= page i) "paginator-active" "")}
(tostring i)])
res))
(if (< page last-page)
- [:a {:href (make-url (+ page 1) query tags)} "→"]
+ [:a {:href (form->path (+ page 1) form)} "→"]
"")]
[:div {} "Всего результатов: " [:strong {} (string.format "%d" total)]]]
""))
-(fn aside-template [query tags paginator]
+(fn aside-template [form paginator]
[:aside {:class "aside"}
[:div {:class "aside-content"}
- (if (or (~= query "") (< 0 (# tags)))
+ (if (not (form-empty? form))
[:a {:href "/" :style "display: block;"}
[:img {:class "logo" :src "static/logo.svg"
:alt "Логотип meicha.ru" :title "Логотип meicha.ru"}]]
[:img {:class "logo" :src "static/logo.svg"
:alt "Логотип meicha.ru" :title "Логотип meicha.ru"}])
[:form {:class "form"}
- [:input {:type :search :name :query :value query
+ [:input {:type :search :name "query" :value form.query
:autofocus true :placeholder "Поисковый запрос"}]
[:div {:class "form-tags"}
[:select {:class "form-tag" :name "tags"}
@@ -220,14 +289,28 @@
(map
(fn [_ tag]
[:option {:value tag
- :selected (if (array.contains tags tag) "selected" nil)}
+ :selected (if (array.contains form.tags tag)
+ "selected" nil)}
tag])
(all-tags)))]]
+ [:div {:class "form-price"}
+ [:input {:type :number :name :min-price :min "1"
+ :placeholder "От ₽" :value (tostring form.min-price)}]
+ [:input {:type :number :name :max-price :min "1"
+ :placeholder "До ₽" :value (tostring form.max-price)}]]
+ [:div {:class "form-price-per"}
+ [:input {:type :checkbox :id "price-per" :name "price-per"
+ :checked (if form.price-per "checked" nil)}]
+ [:label {:for "price-per"} "цена за грамм"]]
[:button {:type :submit} "Искать"]]
paginator]])
-(fn base-template [query tags page total ...]
- (local paginator (paginator-template query tags page 48 total))
+(fn base-template [form page total items]
+ (local paginator (paginator-template form page 48 total))
+ (local spellfix-suggestion
+ (if (and (not (str.empty? form.query)) items (< 0 (# items)))
+ nil
+ (spellfix.guess form.query)))
[:html {:lang "en"}
[:head {}
[:meta {:charset "UTF-8"}]
@@ -236,44 +319,34 @@
[:body {}
[:div {:class "container"}
[:div {:class "content"}
- (aside-template query tags paginator)
+ (aside-template form paginator)
[:section {}
- [:div {:class "list"} ...]
+ (if (< 0 (# items))
+ [:div {:class "list"}
+ (table.unpack (map #(item-template $2) items))]
+ (if spellfix-suggestion
+ [:NO-ESCAPE
+ (string.format texts.no-results-with-suggestion
+ spellfix-suggestion
+ spellfix-suggestion)]
+ texts.no-results))
[:footer {} paginator]]]]]])
-(fn get-query-string [query key]
- (if (and query
- (. query key)
- (. query key 1)
- (< 0 (# (. query key 1))))
- (. query key 1)
- nil))
-
-(fn get-query-number [query key]
- (if (and query
- (. query key)
- (. query key 1))
- (tonumber (. query key 1))
- nil))
-
(fn root-handler [{: path : query}]
(if (= path "/")
(let [headers {:content-type "text/html"}
page (or (get-query-number query "page") 1)
- search (str.trim (or (get-query-string query "query") ""))
- tags (filter #(~= "" $2) (or query.tags []))
+ form (collect-form query)
{: results : total}
- (if (or (~= "" search) (< 0 (# tags)))
- (query-products page search tags)
+ (if (not (form-empty? form))
+ (query-products form page)
{:total 48 :results (random-products 48)})]
(values
200 headers
(html.render
- (base-template
- search tags page total
- (table.unpack (map #(item-template $2) results)))
+ (base-template form page total results)
true)))
(values 404 {} "not found")))
-(luna.router.route "GET /" root-handler)
-(luna.router.static "GET /static/" "static/")
+(must (luna.router.route "GET /" root-handler))
+(must (luna.router.static "GET /static/" "static/"))
diff --git a/lib/array.fnl b/lib/array.fnl
index 6dc0780..8b81c5c 100644
--- a/lib/array.fnl
+++ b/lib/array.fnl
@@ -1,5 +1,9 @@
(import-macros {: reduce} :lib.macro)
+(fn sort [a less-fn]
+ (table.sort a less-fn)
+ a)
+
(fn concat [a b]
(local copy
(fn [a b]
@@ -51,4 +55,4 @@
(local join table.concat)
-{: concat : diff : unique : flatten : join : contains}
+{: sort : concat : diff : unique : flatten : join : contains}
diff --git a/lib/string.fnl b/lib/string.fnl
index 9ac6edd..ca29da0 100644
--- a/lib/string.fnl
+++ b/lib/string.fnl
@@ -1,12 +1,25 @@
-(fn split [string]
- (accumulate [res [] v _
- (string:gmatch "%S+")]
+(local {: must} (require :lib.utils))
+
+(fn empty? [str]
+ (or (= str nil) (= (# str) 0)))
+
+(fn letters [str]
+ (assert
+ (= "string" (type str))
+ (string.format "letters(): str must be string, %s given" (type str)))
+ (var result [])
+ (for [i 1 (must (luna.utf.len str))]
+ (table.insert result (must (luna.utf.sub str i 1))))
+ result)
+
+(fn split [str]
+ (accumulate [res [] v _ (str:gmatch "%S+")]
(do
(table.insert res v)
res)))
-(fn ends-with [string end]
- (= (string.sub string (- (# end))) end))
+(fn ends-with [str end]
+ (= (string.sub str (- (# end))) end))
(fn trim [str]
(str:match "^%s*(.-)%s*$"))
@@ -14,9 +27,9 @@
(fn truncate [str len ellipsis]
(if (and (= (type str) "string")
(< 0 (# str)))
- (if (< (# str) len)
+ (if (< (must (luna.utf.len str)) len)
str
- (.. (trim (luna.utf.sub str 1 len)) (or ellipsis "...")))
+ (.. (trim (must (luna.utf.sub str 1 len))) (or ellipsis "...")))
""))
-{: split : ends-with : trim : truncate}
+{: letters : empty? : split : ends-with : trim : truncate}
diff --git a/spellfix.fnl b/spellfix.fnl
new file mode 100644
index 0000000..3478782
--- /dev/null
+++ b/spellfix.fnl
@@ -0,0 +1,336 @@
+(import-macros {: map : filter} :lib.macro)
+(local array (require :lib.array))
+(local str (require :lib.string))
+
+(local vocabulary ["абрикосовый" "агар" "агарвуд" "агат" "агатис" "алая" "али"
+ "алишань" "алтарь" "альба" "амазонка" "аметист" "амино"
+ "антивирус" "антиквариат" "антикварный" "антилопа" "ань"
+ "аньси" "аньхуа" "аравана" "арахис" "арахисом" "арован"
+ "арована" "аромат" "ароматный" "ароматов" "ароматы" "архат"
+ "архатов" "архаты" "ассортименте" "асфальт" "атомы"
+ "атрибуты" "аура" "бабочка" "бабочки" "багрянник" "базальт"
+ "базовая" "бай" "бакелит" "баклажан" "баклажане" "бамбук"
+ "бамбука" "бамбуке" "бамбуковая" "бамбуковые" "бамбуковый"
+ "бан" "банвэй" "бандун" "банка" "бань" "бао" "баоянь"
+ "барабанчик" "баран" "бацзе" "бегемот" "бежевая" "бежевый"
+ "беззаботный" "бекас" "белая" "белые" "белый" "беседа"
+ "беседка" "бесконечное" "бессмертие" "бессмертия"
+ "бессмертных" "билочунь" "бин" "биндао" "бирюза"
+ "благовоний" "благовония" "благоденствие" "благопожелание"
+ "благостей" "ближе" "блик" "блин" "блина" "блэк" "блюдо"
+ "бмм" "богатство" "богомол" "бодрящий" "бодхи"
+ "бодхидхарма" "боковая" "большая" "большеротая" "больших"
+ "большое" "большой" "борец" "бохун" "бочонка" "бочонок"
+ "браслет" "бронза" "бронзовая" "будда" "будде" "будды"
+ "будет" "буйвол" "булан" "бульдог" "бумаги" "буонарроти"
+ "бусин" "бусина" "бусинмм" "бусины" "бутон" "бутылка"
+ "бутылки" "бхмм" "бык" "быки" "быть" "бьянка" "бэй"
+ "бюрюза" "бяо" "ваджра" "ваза" "вазочка" "вальдшнеп" "ван"
+ "вань" "варки" "васнецов" "ведрко" "великая" "великий"
+ "венге" "версия" "весеннее" "весенние" "весна" "весны"
+ "весы" "ветвях" "ветка" "ветке" "веточка" "вечным" "вижу"
+ "виктор" "винил" "винсент" "вишнвое" "вместе" "внеземной"
+ "вода" "водопад" "воды" "водяная" "водяной" "волнам"
+ "воробей" "воробьиные" "восемь" "восьми" "врата"
+ "встроенным" "выдвижным" "выдержанный" "выращивание"
+ "высокая" "высокий" "высокой" "высокотемпературный"
+ "высший" "вьюнки" "вэй" "вэн" "вэнь" "габа" "габаулун"
+ "габашен" "гайван" "гайвань" "гайваньсиборидаси" "галька"
+ "гамма" "ган" "гань" "гао" "гаомиду" "гаошань" "гармония"
+ "гаруда" "гвоздика" "ггр" "гейша" "геккон" "гималайский"
+ "гинкго" "главных" "гладкий" "глаз" "глазурь" "глазурью"
+ "глина" "глины" "глициния" "гог" "год" "годов" "гои"
+ "голова" "головы" "голубая" "голубой" "голубь" "гор" "гора"
+ "горах" "горизонтом" "горная" "горном" "горный" "горшок"
+ "горшочек" "горы" "гоу" "грамм" "гранат" "гранатами"
+ "граненый" "графит" "грецкий" "гриб" "грибной" "грибом"
+ "грибы" "грин" "грифовая" "груша" "грушевый" "гуа" "гуан"
+ "гуанси" "гуань" "гуаньинь" "гуй" "гун" "гунтин" "гунфу"
+ "гуси" "густав" "гусь" "гушу" "гэланхэ" "дабайхао"
+ "дабайча" "дали" "дан" "дань" "дао" "даос" "даоское"
+ "дарума" "дарумы" "дарующий" "дары" "дасюэшань" "дахунпао"
+ "два" "двоих" "двойное" "дворянский" "денежная" "день"
+ "дерева" "деревня" "дерево" "деревьев" "деревянная"
+ "деревянном" "деревянный" "деревянным" "дескрипторов"
+ "дети" "джу" "джэй" "дикая" "дикие" "дикий" "диких"
+ "дикого" "дикой" "дин" "динь" "диск" "длинный" "для" "дном"
+ "добродушный" "долголетие" "долголетия" "дораэмон" "доска"
+ "достаток" "доу" "драгоценная" "дракон" "дракона"
+ "драконом" "драконхранитель" "драконы" "древа" "древесина"
+ "древний" "дровяной" "дуань" "дуаньни" "дун" "дух" "дымка"
+ "дьен" "дэхуа" "дянь" "дяньхун" "дяочань" "европейский"
+ "естественность" "жаба" "жабадракон" "жабе" "жан" "жаровня"
+ "жасминовый" "жезл" "жезлом" "железо" "желтая" "желтые"
+ "желтый" "жемчуг" "жемчужина" "жемчужиной" "жемчужины"
+ "жен" "жень" "жернов" "жертвенный" "жесть" "жестяная"
+ "жидкое" "жизнь" "жлтая" "жлтой" "жлтый" "жоу" "жуйтюань"
+ "жунтянь" "журавлей" "журавли" "журавль" "жэнь" "забором"
+ "заваривай" "заваривания" "заварочная" "заварочный"
+ "завертка" "загадка" "зайчик" "заря" "затерянные"
+ "затеряться" "заяц" "звезда" "зверк" "зеленая" "зеленый"
+ "зелная" "зелное" "зелной" "зелные" "зелный" "земли"
+ "земляники" "земной" "зернами" "зимний" "зимняя"
+ "зимородок" "зип" "знакомства" "знакомство" "значок"
+ "золота" "золотая" "золоте" "золотистая" "золотистый"
+ "золото" "золотое" "золотой" "золотом" "золотые" "золотым"
+ "зрнах" "игла" "иглы" "игра" "играют" "игрунка" "иероглиф"
+ "иероглифы" "извергающая" "изгиб" "изобилия" "изображением"
+ "изогнутый" "изумрудная" "изумрудные" "изящество" "или"
+ "император" "императора" "индэ" "инженерное" "инкрустацией"
+ "инструмент" "инструментов" "инструменты" "инь" "исинская"
+ "исинский" "исинской" "искусство" "истинная" "истинно"
+ "история" "йеллоу" "йон" "йоу" "каждый" "каи" "каллиграфии"
+ "каллиграфия" "камень" "камне" "камня" "камфара"
+ "камфорный" "камфоры" "као" "капля" "капюшоне" "карамель"
+ "карамельный" "карпы" "картина" "картины" "кашпо"
+ "каштановая" "каштаны" "квадрат" "квадратная" "квадратное"
+ "квадратный" "кварц" "кедр" "керамика" "керамики" "кибер"
+ "киноварная" "киноварный" "киноварь" "киноварьбхмм"
+ "кипячения" "кирпич" "кирпичик" "кисет" "кистей" "кисть"
+ "китайский" "китайским" "классика" "классическая" "клетка"
+ "климт" "клипса" "книгами" "кнопкой" "коала" "кобальт"
+ "кобальтовая" "кобальтовые" "кобальтовый" "ковка" "кожа"
+ "колба" "колбы" "коленце" "коллекционная" "колодец"
+ "колокол" "колосья" "колотый" "кольцами" "кольцо" "кольцом"
+ "комбо" "комковой" "комплект" "конверт" "консоль" "контур"
+ "конусов" "коралл" "корень" "корзина" "корзинка" "корица"
+ "коричневая" "коричневое" "коричневый" "кормилица"
+ "коробка" "коробке" "коробочка" "коробочке" "корпусе"
+ "кости" "кость" "костяной" "кот" "котелок" "котик" "котики"
+ "котнок" "краб" "крабы" "край" "красавица" "красная"
+ "краснобелый" "красного" "красное" "красной" "красносиний"
+ "красные" "красный" "красных" "кратер" "крафткартон"
+ "креветка" "креветки" "кристалл" "кровавый" "кровать"
+ "кролик" "кролики" "крольчонок" "круг" "круглая" "круглое"
+ "круглый" "круглым" "кружка" "крупная" "крыло" "крыса"
+ "крыски" "крышки" "крышкой" "крышку" "крышу" "куан" "кубок"
+ "кувшин" "кузнечик" "куй" "культура" "кун" "кунь" "купаж"
+ "курильница" "куропатка" "куропатки" "кучжушань" "кха"
+ "лабранг" "лавр" "лазуревая" "лазурит" "лазурь" "лак"
+ "лаконичная" "лаконичное" "лаконичный" "лампада" "лан"
+ "лань" "ланьбмм" "ланьянь" "лао" "лаодуань" "лаошу"
+ "латунь" "лев" "лед" "ледяная" "лежанка" "лепестки"
+ "лепешка" "лес" "леса" "лесная" "лесной" "летающая"
+ "летний" "летучая" "летучей" "летучие" "ликов" "лилия"
+ "лимин" "лин" "линчжи" "линь" "линьцан" "линьцана" "лист"
+ "листве" "листе" "листопад" "листья" "листьях" "личи"
+ "лодка" "лопаточка" "лоск" "лотос" "лотоса" "лотосе"
+ "лотосовом" "лотосовый" "лотосом" "лотосы" "лошадей"
+ "лошадь" "лун" "луна" "лунная" "лунный" "луны" "львов"
+ "львы" "льду" "любимого" "люйтан" "люкс" "люфа" "лягушка"
+ "лягушки" "лян" "лянь" "магнолии" "магнолия" "магритт"
+ "май" "майтрейя" "малая" "маленькая" "малые" "малый"
+ "мальки" "мальчик" "мандарине" "мандаринки" "маньлан"
+ "маньно" "мао" "маофэн" "марка" "мастер" "маття" "матура"
+ "матча" "медитации" "медитирующий" "медная" "медный"
+ "медовые" "медь" "между" "мей" "менхай" "менхайский"
+ "меняющая" "металл" "металлик" "металлической" "мешочек"
+ "микеланджело" "микрофибра" "мин" "миндаль" "мини"
+ "мининабор" "миниточа" "минитун" "минцзян" "мир" "мира"
+ "мистер" "ммб" "мокрый" "моллюск" "молочная" "монах"
+ "монетами" "монете" "монетка" "монеты" "мопс" "море" "моша"
+ "мощный" "мощь" "мудрец" "мудрый" "мун" "муравей" "мушмула"
+ "мыши" "мышки" "мышонок" "мышь" "мышью" "мэй" "мэйхуа"
+ "мэн" "мэнку" "мэнсун" "мэнхай" "мэнхайская" "мэнхая"
+ "мятный" "набор" "награда" "нака" "наклейки" "нан" "нань"
+ "наньно" "наньтоу" "наполированный" "наработанный"
+ "нарухина" "нарцисс" "нарциссы" "наследие" "настроение"
+ "натюрморт" "наук" "начало" "небесной" "небесный" "небо"
+ "неведомый" "нежная" "нежный" "нектар" "нержавеющая"
+ "нефрит" "нефритовая" "нефритовое" "нижним" "низкий"
+ "нитка" "нить" "новая" "новый" "нож" "ножках" "ножке" "нос"
+ "ночи" "ночь" "нун" "нюгай" "оазис" "обеденный" "обезьян"
+ "обезьянка" "обезьянки" "обезьяны" "обжиг" "облака"
+ "облаками" "облаках" "облако" "ободком" "обратных"
+ "обсидиан" "овал" "овальная" "овальный" "огненная"
+ "огнеупорная" "огнеупорного" "огнеупорное" "одуванчик"
+ "озера" "окаменелая" "океана" "окихиро" "округлый"
+ "олдскул" "олень" "олово" "оплетка" "оплеткой" "оплткой"
+ "оранжевой" "оранжевый" "органик" "органическая" "орех"
+ "орехи" "ореховый" "орнамент" "орнаментом" "орхидеи"
+ "орхидея" "осень" "осетр" "османтус" "основных" "особая"
+ "отбивное" "отдых" "охраняющий" "очиститель" "пабло"
+ "павлин" "пакет" "пакетов" "палисандр" "палисандра"
+ "палочка" "палочки" "панда" "панцирь" "пань" "пао" "пара"
+ "парный" "пару" "парча" "патра" "пачка" "пейзаж"
+ "пейзажная" "пеликан" "первый" "перепелиное"
+ "перепрыгивают" "перламутром" "персик" "персика" "персике"
+ "персики" "персиковый" "персиком" "персон" "персоны"
+ "петух" "печать" "пиал" "пиала" "пиалу" "пиалы" "пикассо"
+ "пин" "пиньгэ" "пион" "пиона" "пионы" "пирамида" "письмо"
+ "пламя" "плант" "плантационный" "плантация" "пластиковое"
+ "пластиковым" "пластина" "пластинки" "плато" "плетеное"
+ "плетное" "плетной" "плитка" "плоды" "пломбир" "плоская"
+ "побеги" "погружение" "под" "подарочная" "подарочной"
+ "подачей" "подводный" "поддоном" "поднос" "подносом"
+ "подсвечник" "подставка" "подставкаподогреватель"
+ "подставке" "подставки" "подставкой" "подушкой" "познания"
+ "покой" "поколений" "покров" "полной" "полный" "полотенце"
+ "полотно" "полочка" "полуавтоматическая" "полудикий"
+ "полудиких" "полусладкий" "помпа" "пороснок" "постижение"
+ "посуды" "поталь" "походная" "походный" "почка" "праздник"
+ "предел" "предмет" "предметов" "премиум" "пресс" "природе"
+ "пробуждение" "прозрачная" "прозрачный" "прохладе"
+ "процветание" "прочность" "пруд" "пруду" "пряжка"
+ "прямоугольная" "прямоугольный" "прятки" "птица" "птицы"
+ "птички" "пурпур" "пурпурный" "путь" "пушинка" "пуэр"
+ "пуэра" "пуэрмобиль" "пуэров" "пчела" "пышная" "пяо"
+ "пятнистый" "пять" "работа" "работы" "радости" "разделки"
+ "размер" "размышлений" "райские" "раковина" "ранневесенний"
+ "распускающиеся" "распустившиеся" "расслабляющий"
+ "рассыпной" "растительный" "раухтопаз" "реактор" "ребенок"
+ "ребнок" "регион" "резвящихся" "резьба" "река" "реки"
+ "рельеф" "рене" "репа" "рецепт" "рештка" "риса" "рисовый"
+ "рисовыми" "рисовых" "рисунком" "ритуальный" "рог"
+ "рододендрон" "роза" "розовый" "розы" "ромашке" "роса"
+ "роспись" "россия" "руб" "рубин" "рудра" "рутилом" "ручка"
+ "ручками" "ручкой" "ручная" "рыба" "рыбадракон" "рыбаки"
+ "рыбка" "рыбки" "рыбой" "рыбы" "рыжая" "рыжий" "саванна"
+ "сад" "сакральной" "сакура" "саламандра" "салатовая"
+ "салфетница" "сандал" "сандала" "саоган" "сахарок" "сбор"
+ "сверчок" "свет" "светлая" "светлое" "светлорыжая"
+ "светлые" "светлый" "свинка" "свиньи" "свинья" "свитком"
+ "связь" "священного" "северный" "секции" "селадон" "семена"
+ "семь" "семян" "серая" "сердечки" "сердолик" "сердца"
+ "сердце" "серебре" "серебро" "серебряная" "серебряные"
+ "серия" "серое" "серый" "сиборидаси" "сигара" "сизоворонка"
+ "сила" "силу" "символы" "син" "синебежевый" "синее" "синей"
+ "синие" "синий" "синь" "синяя" "сипин" "сиреневый" "сито"
+ "ситом" "сифон" "сияние" "склада" "скоропись" "скрученный"
+ "скульптура" "сладкий" "сладость" "слив" "слива" "сливник"
+ "сливы" "слитке" "слитком" "слово" "сложная" "слон"
+ "слоновой" "слоном" "слоны" "смола" "сна" "снежная"
+ "снежный" "собака" "советский" "совок" "совочек"
+ "согревающий" "солнца" "солнце" "сорт" "сосна" "сосны"
+ "сосуд" "спирали" "спираль" "спиртовка" "спокойствие"
+ "спящий" "средняя" "стакан" "стакантермос" "сталь"
+ "станция" "старая" "старого" "старое" "старый" "старых"
+ "статуэтка" "стекла" "стекло" "стеклянная" "стекляный"
+ "стикер" "стикерпак" "стикеры" "стиле" "стилов" "стиль"
+ "сто" "стол" "столик" "столика" "стопы" "странствующий"
+ "стрекоза" "строк" "стул" "стула" "ступа" "суаньджи" "суй"
+ "сумка" "сумо" "сун" "сундук" "сунская" "сунском" "сунь"
+ "суровая" "сутра" "сутры" "сухого" "сухое" "сферы"
+ "счастливая" "счастливое" "счастливый" "счастье" "счастья"
+ "сытый" "сюань" "сюнь" "сюэ" "сягуань" "сян" "сянь"
+ "сяньхуа" "сяо" "тай" "тайванский" "тайвань" "тайваньская"
+ "тайваньского" "тайхэ" "такинава" "тан" "танская" "танские"
+ "танцуют" "тань" "тао" "тарелка" "тарзан" "творцы" "твоя"
+ "тегуаньинь" "телец" "темная" "темное" "темнокоричневая"
+ "тентяо" "термоз" "термос" "термоса" "термоскувшин" "тигр"
+ "тигра" "тигровый" "тин" "типот" "тираж" "тисбмм" "титан"
+ "титестера" "тишине" "ткань" "тмная" "тмное" "тмные"
+ "тмный" "тобой" "тонкое" "тоу" "точа" "трав" "травы"
+ "трапеция" "треножник" "трепет" "треугольная" "трехлапая"
+ "три" "трио" "трионикс" "троих" "трон" "трона" "трубками"
+ "трубкой" "трхлапая" "тулоу" "тун" "тунь" "тушечница"
+ "тушью" "тхи" "тыква" "тьен" "тюльпан" "тяван" "тян" "тянь"
+ "тяоша" "угря" "удачи" "узор" "улун" "улунов" "улуны"
+ "улыбка" "уляншань" "упаковка" "уруши" "уся" "утешения"
+ "утнок" "уточки" "утро" "утун" "ухват" "уцай" "учатся"
+ "ушастый" "фан" "фаньгу" "фарфор" "фарфора" "фарфоровая"
+ "феникс" "феникса" "фениксовый" "фермерский" "фигурка"
+ "фильтрпакеты" "финиковый" "фиолетового" "фиолетовый"
+ "фирма" "флакон" "форма" "формикарий" "фосян" "фруктовый"
+ "фугу" "фудин" "фун" "футболка" "фуцзянь" "фуцзяньском"
+ "фэй" "фэйчжоу" "фэн" "фэнхуан" "фэнцин" "фэншуй" "хай"
+ "хайвань" "халат" "ханой" "хань" "ханьской" "хао" "хгр"
+ "хищная" "хлопок" "хлопушками" "хлоритом" "хмм" "хомяк"
+ "хороший" "хотеи" "хотей" "хотэй" "хоу" "хохин" "хранения"
+ "хранящий" "хризантема" "хризантемы" "хрусталь" "хсм" "хуа"
+ "хуан" "хуацзинань" "хун" "хунни" "хуолун" "хупитан"
+ "хурма" "хурмы" "хуэй" "ххсм" "хэй" "хэйтан" "хэйхуали"
+ "хэйча" "хэн" "хэтян" "хэхуань" "цан" "цаплей" "цапля"
+ "царь" "цвет" "цвета" "цветами" "цветов" "цветок"
+ "цветочки" "цветочная" "цветочный" "цветущие" "цветущий"
+ "цветы" "целеустремленность" "цельная" "церемонии" "цза"
+ "цзао" "цзе" "цзи" "цзин" "цзингу" "цзиндэчжень"
+ "цзиндэчжэнь" "цзиндэчжэньгоры" "цзинмай" "цзиношань"
+ "цзинь" "цзиньгу" "цзиньдечжень" "цзиньдэчжень"
+ "цзиньдэчжэнь" "цзу" "цзун" "цзы" "цзыни" "цзытан" "цзю"
+ "цзюй" "цзюнь" "цзюэнь" "цзя" "цзянь" "цзяньшуй"
+ "цзяньшуйская" "цзяо" "цикада" "цикадки" "цикады" "цикл"
+ "цилиндр" "цилинь" "цимень" "цин" "цинния" "цинхуа"
+ "циньчжоу" "цуй" "цун" "цюй" "цюэ" "цяньлян" "цяо" "чабани"
+ "чабань" "чаегуань" "чаем" "чаепития" "чай" "чайнатаун"
+ "чайная" "чайник" "чайника" "чайники" "чайником"
+ "чайниксифон" "чайница" "чайничек" "чайного" "чайное"
+ "чайной" "чайные" "чайный" "чайных" "чак" "чаньчжи"
+ "чаочжоу" "чато" "чахай" "чаху" "чахэ" "чацзя" "чаша"
+ "чашао" "чашек" "чашка" "чашкатермо" "чашке" "чашки" "чая"
+ "чен" "черенками" "черепаха" "черепахадракон" "черепаший"
+ "черный" "черпак" "четки" "четыре" "чехлом" "чжай" "чжан"
+ "чжанлан" "чжань" "чжаоцзюнь" "чжен" "чжень" "чжи" "чжима"
+ "чжу" "чжуань" "чжун" "чжуни" "чжэн" "чжэнь" "чизкейк"
+ "чилл" "чин" "чиновника" "чиновников" "чицзыму" "чоу"
+ "чрная" "чрное" "чрный" "чтки" "чток" "чугун" "чудо" "чун"
+ "чунь" "чуньхуа" "чха" "чхан" "чхин" "чхэн" "чхэнсян" "чэн"
+ "чэнь" "чэньсян" "чэньсяо" "шай" "шайхун" "шан" "шань"
+ "шар" "шаром" "шары" "шен" "шестигранный" "шестиугольник"
+ "шило" "шипяо" "шисянь" "шиюань" "шкатулка" "шкаф"
+ "шкафчик" "школа" "шнурок" "шоколад" "шоу" "шрек" "шуй"
+ "шуйпин" "шуйсянь" "шумахер" "шупу" "шурикен" "шутливый"
+ "шуэй" "шэн" "шэнтай" "шэнши" "щипцы" "экстра"
+ "электрический" "электронные" "элементов" "юйчэн" "юлэ"
+ "юндэ" "юнь" "юньнани" "юньнань" "юпитер" "юэнань" "яблоко"
+ "ядерный" "язычки" "яйцо" "якотза" "яма" "янгуйфэй"
+ "янтарная" "янтарь" "янтаряв" "янь" "японская" "японский"
+ "ясный" "яшма"])
+
+(fn levenshtein [str1 str2]
+ (let [len1 (luna.utf.len str1)
+ len2 (luna.utf.len str2)
+ matrix {}]
+ (var cost 0)
+ (if (= len1 0) (lua "return len2")
+ (= len2 0) (lua "return len1")
+ (= str1 str2) (lua "return 0"))
+ (for [i 0 len1] (tset matrix i {}) (tset (. matrix i) 0 i))
+ (for [j 0 len2] (tset (. matrix 0) j j))
+ (for [i 1 len1]
+ (for [j 1 len2]
+ (if (= (luna.utf.sub str1 i 1) (luna.utf.sub str2 j 1))
+ (set cost 0) (set cost 1))
+ (tset (. matrix i) j
+ (math.min (+ (. matrix (- i 1) j) 1) (+ (. matrix i (- j 1)) 1)
+ (+ (. matrix (- i 1) (- j 1)) cost)))))
+ (. matrix len1 len2)))
+
+(fn diff-letters [str1 str2]
+ (# (array.diff (str.letters str1) (str.letters str2))))
+
+(fn guess [string]
+ (local result
+ (array.join
+ (map
+ (fn [_ token]
+ (local index
+ (map (fn [_ v]
+ {:word v :distance (levenshtein token v)})
+ vocabulary))
+ (local sorted-index
+ (array.sort index #(< $1.distance $2.distance)))
+ (local most-similar (. sorted-index 1))
+ (if (< most-similar.distance 3)
+ (do
+ ;; compare letters in candidates to choose the best option
+ (local same-distance-words
+ (filter
+ #(= $2.distance most-similar.distance)
+ sorted-index))
+ (local sorted-candidates
+ (array.sort
+ (map
+ (fn [_ v] {:word v.word
+ :diff-letters (diff-letters token v.word)})
+ same-distance-words)
+ #(< $1.diff-letters $2.diff-letters)))
+ (. sorted-candidates 1 :word))
+ token))
+ (str.split string))
+ " "))
+ (if (~= result string)
+ result
+ nil))
+
+{: guess}
diff --git a/texts.fnl b/texts.fnl
new file mode 100644
index 0000000..f30c037
--- /dev/null
+++ b/texts.fnl
@@ -0,0 +1,2 @@
+{:no-results "По вашему запросу нет результатов. Попробуйте изменить параметры поиска."
+ :no-results-with-suggestion "По вашему запросу нет результатов. Возможно, вы имели в виду <a href=\"?query=%s\">%s</a>?"}