From 1204496efa2fcd495bd74ba8ca249b7f082f3ba5 Mon Sep 17 00:00:00 2001 From: unwox Date: Tue, 15 Oct 2024 14:48:16 +0600 Subject: WIP try to fix spelling mistakes in search querie currently works very slowly and uses a lot of CPU --- bin/serve.fnl | 237 ++++++++++++++++++++++++++-------------- lib/array.fnl | 6 +- lib/string.fnl | 29 +++-- spellfix.fnl | 336 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ texts.fnl | 2 + 5 files changed, 519 insertions(+), 91 deletions(-) create mode 100644 spellfix.fnl create mode 100644 texts.fnl 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 """ "\"") (string.gsub "&" "&")))) +(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, '', '') 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, '', '') 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 "По вашему запросу нет результатов. Возможно, вы имели в виду %s?"} -- cgit v1.2.3