diff --git a/Cargo.lock b/Cargo.lock index feb6704..565fba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -793,6 +793,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bitpacking" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +dependencies = [ + "crunchy", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -913,9 +922,17 @@ version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + [[package]] name = "cfg-if" version = "1.0.0" @@ -1122,6 +1139,8 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "tantivy", + "tempfile", "tracing", "tracing-subscriber", "uuid", @@ -1185,6 +1204,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -1228,6 +1256,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1370,6 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1436,6 +1471,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dtoa" version = "1.0.9" @@ -1547,6 +1588,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastdivide" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b" + [[package]] name = "fastrand" version = "2.1.1" @@ -1591,6 +1638,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1600,6 +1653,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" +dependencies = [ + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1848,6 +1911,11 @@ name = "hashbrown" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -1915,6 +1983,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.12" @@ -2064,6 +2138,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "io-uring" version = "0.6.4" @@ -2104,6 +2190,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.72" @@ -2168,6 +2263,12 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" version = "0.2.161" @@ -2299,6 +2400,21 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.0", +] + +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" + [[package]] name = "matchers" version = "0.1.0" @@ -2324,12 +2440,31 @@ dependencies = [ "digest", ] +[[package]] +name = "measure_time" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "migration" version = "0.1.0" @@ -2396,6 +2531,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + [[package]] name = "napi" version = "2.16.13" @@ -2563,6 +2704,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "object" version = "0.36.5" @@ -2578,6 +2729,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oneshot" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2646,6 +2803,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownedbytes" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "parcel_selectors" version = "0.27.0" @@ -3085,6 +3251,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rayon" version = "1.10.0" @@ -3452,6 +3628,16 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3964,6 +4150,15 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.9" @@ -4257,6 +4452,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static-self" version = "0.1.1" @@ -4335,6 +4536,147 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tantivy" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64 0.22.1", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "itertools 0.12.1", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.12.1", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax 0.8.5", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" +dependencies = [ + "nom", +] + +[[package]] +name = "tantivy-sstable" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" +dependencies = [ + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" +dependencies = [ + "murmurhash32", + "rand_distr", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" +dependencies = [ + "serde", +] + [[package]] name = "tap" version = "1.0.1" @@ -4703,6 +5045,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "utf8parse" version = "0.2.2" @@ -5132,3 +5480,31 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index bbf4431..253695f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,5 +25,7 @@ humantime = "2.1.0" humantime-serde = "1.1.1" actix-session = { version = "0.10.1", features = ["redis-session-rustls"] } actix-identity = "0.8.0" +tantivy = "0.22.0" +tempfile = "3.13.0" [build-dependencies] diff --git a/assets/styles.css b/assets/styles.css index f79f65a..280e6c0 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -1744,10 +1744,6 @@ html{ margin-right: auto; } -.mt-6{ - margin-top: 1.5rem; -} - .block{ display: block; } @@ -1888,30 +1884,16 @@ html{ border-bottom-width: 1px; } -.border-r-4{ - border-right-width: 4px; -} - .border-gray-100{ --tw-border-opacity: 1; border-color: rgb(243 244 246 / var(--tw-border-opacity)); } -.border-primary{ - --tw-border-opacity: 1; - border-color: rgb(255 99 99 / var(--tw-border-opacity)); -} - .bg-gray-300{ --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } -.bg-secondary-100{ - --tw-bg-opacity: 1; - background-color: rgb(226 226 213 / var(--tw-bg-opacity)); -} - .stroke-current{ stroke: currentColor; } @@ -1993,11 +1975,6 @@ html{ color: rgb(55 65 81 / var(--tw-text-opacity)); } -.text-secondary-200{ - --tw-text-opacity: 1; - color: rgb(136 136 131 / var(--tw-text-opacity)); -} - .text-stone-500{ --tw-text-opacity: 1; color: rgb(120 113 108 / var(--tw-text-opacity)); @@ -2108,32 +2085,16 @@ html{ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.hover\:scale-110:hover{ - --tw-scale-x: 1.1; - --tw-scale-y: 1.1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - .hover\:bg-gray-400:hover{ --tw-bg-opacity: 1; background-color: rgb(156 163 175 / var(--tw-bg-opacity)); } -.hover\:bg-opacity-60:hover{ - --tw-bg-opacity: 0.6; -} - .hover\:text-gray-600:hover{ --tw-text-opacity: 1; color: rgb(75 85 99 / var(--tw-text-opacity)); } -.hover\:shadow-inner:hover{ - --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - .hover\:shadow-lg:hover{ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); @@ -2175,10 +2136,6 @@ html{ grid-column: span 3 / span 3; } - .md\:block{ - display: block; - } - .md\:flex{ display: flex; } diff --git a/src/actors/mod.rs b/src/actors/mod.rs new file mode 100644 index 0000000..a557bff --- /dev/null +++ b/src/actors/mod.rs @@ -0,0 +1 @@ +pub mod search; diff --git a/src/actors/search.rs b/src/actors/search.rs new file mode 100644 index 0000000..4f7bb61 --- /dev/null +++ b/src/actors/search.rs @@ -0,0 +1,129 @@ +use std::sync::{Arc, Mutex}; + +use actix::prelude::*; +use tantivy::collector::TopDocs; +use tantivy::query::QueryParser; +use tantivy::{doc, Index, IndexWriter, ReloadPolicy, Searcher}; +use tantivy::{schema::*, TantivyError}; + +pub struct Inner { + writer: IndexWriter, + schema: Schema, + searcher: Searcher, + index: Index, +} + +pub struct SearchEngine(Arc>); + +impl SearchEngine { + pub fn build() -> Result { + let index_path = std::path::Path::new("./indices"); + let mut schema_builder = Schema::builder(); + schema_builder.add_u64_field("id", INDEXED); + schema_builder.add_text_field("title", TEXT); + schema_builder.add_text_field("summary", TEXT); + + let schema = schema_builder.build(); + let index = Index::create_in_dir(&index_path, schema.clone())?; + let index_writer: IndexWriter = index.writer(50_000_000)?; + + let reader = index + .reader_builder() + .reload_policy(ReloadPolicy::OnCommitWithDelay) + .try_into() + .unwrap(); + let searcher = reader.searcher(); + + Ok(Self(Arc::new(Mutex::new(Inner { + writer: index_writer, + schema, + searcher, + index, + })))) + } +} + +impl actix::Actor for SearchEngine { + type Context = actix::Context; +} + +#[derive(Debug, Message)] +#[rtype(result = "Result")] +pub struct CreateRecipe { + id: u64, + title: String, + summary: String, +} + +impl Handler for SearchEngine { + type Result = actix::ResponseActFuture>; + + fn handle(&mut self, msg: CreateRecipe, _ctx: &mut Self::Context) -> Self::Result { + let inner = self.0.clone(); + Box::pin( + async move { + let mut shared = inner.lock().unwrap(); + + let id = shared.schema.get_field("id").unwrap(); + let title = shared.schema.get_field("summary").unwrap(); + let summary = shared.schema.get_field("summary").unwrap(); + + let n = shared.writer.add_document(doc! { + id => msg.id, + title => msg.title, + summary => msg.summary, + })?; + shared.writer.commit()?; + + Ok(n) + } + .into_actor(self), + ) + } +} + +#[derive(Debug, Message)] +#[rtype(result = "Result,TantivyError>")] +pub struct Find { + query: String, +} + +impl Handler for SearchEngine { + type Result = actix::ResponseActFuture, TantivyError>>; + + fn handle(&mut self, msg: Find, _ctx: &mut Self::Context) -> Self::Result { + let inner = self.0.clone(); + Box::pin( + async move { + let shared = inner.lock().unwrap(); + + let id = shared.schema.get_field("id").unwrap(); + let title = shared.schema.get_field("summary").unwrap(); + let summary = shared.schema.get_field("summary").unwrap(); + + let query_parser = QueryParser::for_index(&shared.index, vec![title, summary]); + let query = query_parser.parse_query(&msg.query)?; + + let rows = shared.searcher.search(&query, &TopDocs::with_limit(100))?; + + let ids = rows + .into_iter() + .filter_map(|row| { + let doc: Option = shared.searcher.doc(row.1).ok(); + doc + }) + .fold(Vec::with_capacity(1_000), |agg, doc| { + doc.get_all(id) + .filter_map(|id| id.as_u64()) + .fold(agg, |mut agg, id| { + agg.push(id); + agg + }) + }); + + Ok(ids) + } + .into_actor(self), + ) + } +} diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 0000000..8fc5729 --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,17 @@ +use crate::types::Page; + +pub fn duration(sec: &&i32) -> ::askama::Result { + use humantime::format_duration; + use std::time::Duration; + + Ok(format_duration(Duration::from_secs(**sec as u64)).to_string()) +} + +pub fn page_class(pair: &(Page, Page)) -> ::askama::Result { + tracing::info!("page_class: {pair:?}"); + Ok(match pair { + (current, expected) if current == expected => " border-r-4 border-primary ", + _ => "", + } + .into()) +} diff --git a/src/main.rs b/src/main.rs index 48d2653..dcbfbba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,266 +1,20 @@ use actix_files::Files; -use actix_identity::{Identity, IdentityMiddleware}; +use actix_identity::IdentityMiddleware; use actix_session::{storage::RedisSessionStore, SessionMiddleware}; use actix_web::cookie::Key; -use actix_web::{ - get, post, - web::{Data, Form, Json, Path}, - App, HttpResponse, HttpServer, Responder, -}; -use actix_web::{HttpMessage, HttpRequest}; -use askama::Template; -use sea_orm::{prelude::*, Database, QuerySelect}; -use serde::Deserialize; +use actix_web::{web::Data, App, HttpServer}; +use sea_orm::Database; use std::str::FromStr; +use types::Admins; -mod entities; -mod filters { - pub fn duration(sec: &&i32) -> ::askama::Result { - use humantime::format_duration; - use std::time::Duration; - - Ok(format_duration(Duration::from_secs(**sec as u64)).to_string()) - } -} +pub mod actors; +pub mod entities; +pub mod filters; +pub mod routes; +pub mod types; const SESSION_KEY: &'static str = "session"; -type User = String; - -#[derive(Debug, PartialEq, Clone, Copy)] -enum Page { - Index, - Recipe, - Search, - SignIn, -} - -#[derive(Debug, Template, derive_more::Deref)] -#[template(path = "recipe_card.html")] -struct RecipeCard(entities::recipies::Model); - -#[derive(Debug, Template)] -#[template(path = "index.html")] -struct IndexTemplate { - recipies: Vec, - count: u64, - session: Option, - page: Page, -} -#[derive(Debug, Template)] -#[template(path = "top_bar.html")] -struct TopBar<'s> { - session: &'s Option, -} -impl<'s> TopBar<'s> { - pub fn new(session: &'s Option) -> Self { - tracing::info!("top bar session: {session:?}"); - Self { session } - } -} - -#[derive(Debug)] -pub struct Admin { - pub email: String, - pub pass: String, -} - -impl FromStr for Admin { - type Err = (); - - fn from_str(s: &str) -> Result { - let mut it = s.split(':'); - - Ok(Self { - email: it.next().expect("Admin login is required").into(), - pass: it.next().expect("Admin password is required").into(), - }) - } -} - -#[derive(Debug)] -pub struct Admins(Vec); - -impl FromStr for Admins { - type Err = (); - fn from_str(s: &str) -> Result { - Ok(Self( - s.trim().split(',').filter_map(|s| s.parse().ok()).collect(), - )) - } -} - -#[derive(Debug, serde::Deserialize)] -struct CreateRecipe { - // #[serde(default)] - // #[serde(with = "humantime_serde")] - // time: Option, -} - -#[derive(Debug, serde::Deserialize)] -struct SignIn { - email: String, - password: String, -} - -#[derive(Debug, Template)] -#[template(path = "sign_in/form.html")] -struct SignInForm { - not_found: bool, - email: String, - session: Option, - page: Page, -} - -#[get("/sign-in")] -async fn render_sign_in() -> SignInForm { - SignInForm { - not_found: false, - email: "".into(), - session: None, - page: Page::SignIn, - } -} - -#[post("/sign-in")] -async fn sign_in( - req: HttpRequest, - admins: Data, - payload: Form, - admin: Option, -) -> HttpResponse { - let payload = payload.into_inner(); - let is_admin = admins - .as_ref() - .0 - .iter() - .any(|admin| admin.email == payload.email && admin.pass == payload.password); - if is_admin { - tracing::info!("Valid credentials"); - // let res = session - // .insert(SESSION_KEY, payload.email.clone()) - // .inspect_err(|e| tracing::error!("Failed to save session: {e}")); - // tracing::debug!("Saving session res: {res:?}"); - let _s = - Identity::login(&req.extensions(), payload.email).expect("Failed to store session"); - - HttpResponse::SeeOther() - .append_header(("location", "/")) - .finish() - } else { - tracing::warn!("Invalid credentials"); - HttpResponse::BadRequest() - .append_header(("Content-Type", "text/html")) - .body( - SignInForm { - email: payload.email, - not_found: true, - session: admin.and_then(|s| s.id().ok()), - page: Page::SignIn, - } - .render() - .unwrap_or_default(), - ) - } -} - -#[derive(Debug, Deserialize)] -struct Padding { - page: Option, -} - -#[get("/")] -async fn index_html( - db: Data, - q: actix_web::web::Query, - admin: Option, -) -> IndexTemplate { - let count = (entities::prelude::Recipies::find() - .count(&**db) - .await - .unwrap_or_default() as f64 - / 20.0) - .ceil() as u64; - let recipies = entities::prelude::Recipies::find() - .limit(20) - .offset(q.page.unwrap_or_default() as u64) - .all(&**db) - .await - .unwrap_or_default(); - - IndexTemplate { - recipies: recipies.into_iter().map(RecipeCard).collect(), - count, - session: admin.and_then(|s| s.id().ok()), - page: Page::Index, - } -} - -#[derive(Debug, Template)] -#[template(path = "recipies/show.html")] -struct RecipeDetailTemplate { - recipe: entities::recipies::Model, - tags: Vec, - steps: Vec, - ingeredients: Vec, - session: Option, - page: Page, -} - -#[get("/recipe/{id}")] -async fn show( - id: Path, - db: Data, - admin: Option, -) -> impl Responder { - let id = id.into_inner(); - let db = &**db; - let Ok(Some(recipe)) = entities::prelude::Recipies::find() - .filter(entities::recipies::Column::Id.eq(id)) - .one(db) - .await - else { - return HttpResponse::SeeOther() - .append_header(("location", "/")) - .finish(); - }; - let tags = entities::prelude::RecipeTags::find() - .filter(entities::recipe_tags::Column::RecipeId.eq(id)) - .all(db) - .await - .unwrap_or_default(); - let steps = entities::prelude::RecipeSteps::find() - .filter(entities::recipe_steps::Column::RecipeId.eq(id)) - .all(db) - .await - .unwrap_or_default(); - let ingeredients = entities::prelude::RecipeIngeredients::find() - .filter(entities::recipe_ingeredients::Column::RecipeId.eq(id)) - .all(db) - .await - .unwrap_or_default(); - - HttpResponse::Ok().body( - RecipeDetailTemplate { - steps, - tags, - recipe, - ingeredients, - session: admin.and_then(|s| s.id().ok()), - page: Page::Recipe, - } - .render() - .unwrap_or_default(), - ) -} - -#[get("/styles.css")] -async fn styles_css() -> HttpResponse { - HttpResponse::Ok() - .append_header(("Content-Type", "text/css")) - .body(include_str!("../assets/styles.css")) -} - #[actix_web::main] async fn main() { let _ = tracing_subscriber::fmt::init(); @@ -280,7 +34,7 @@ async fn main() { std::env::var("PSQL").expect("PSQL is required. Please provide postgresql connection url"); let redis_url = std::env::var("REDIS").expect("REDIS is required. Pleasde provide redis connection url"); - let secret = + let mut secret = std::env::var("SECRET").expect("SECRET is required. Please provider encryption key"); // Build structs @@ -295,6 +49,11 @@ async fn main() { } let redis = redis::Client::open(redis_url.as_str()).expect("Failed to connect to redis"); let secret_key = Key::from(secret.as_bytes()); + unsafe { + for c in secret.as_bytes_mut() { + *c = 0; + } + }; drop(secret); tracing::info!("{:?}", secret_key.master()); let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap(); @@ -321,12 +80,8 @@ async fn main() { .app_data(admins.clone()) .app_data(db.clone()) .app_data(redis.clone()) - .service(styles_css) .service(Files::new("/pages", "./pages")) - .service(render_sign_in) - .service(sign_in) - .service(index_html) - .service(show) + .configure(routes::configure) }) .bind(&bind) .expect("Failed to start http server") diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..74a45ea --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,256 @@ +use crate::types::*; +use crate::{entities, filters}; +use actix_identity::Identity; +use actix_web::web::{Data, Form, Path}; +use actix_web::HttpMessage; +use actix_web::{get, post, HttpRequest, HttpResponse, Responder}; +use askama::Template; +use sea_orm::{prelude::*, QuerySelect}; +use serde::Deserialize; + +#[derive(Debug, Template, derive_more::Deref)] +#[template(path = "recipe_card.html")] +struct RecipeCard(entities::recipies::Model); + +#[derive(Debug, Template)] +#[template(path = "index.html")] +struct IndexTemplate { + recipies: Vec, + count: u64, + session: Option, + page: Page, +} + +#[derive(Debug, Template)] +#[template(path = "search.html")] +struct SearchTemplate { + recipies: Vec, + count: u64, + query: String, + session: Option, + page: Page, +} + +#[derive(Debug, Template)] +#[template(path = "top_bar.html")] +struct TopBar<'s> { + session: &'s Option, +} +impl<'s> TopBar<'s> { + pub fn new(session: &'s Option) -> Self { + tracing::info!("top bar session: {session:?}"); + Self { session } + } +} + +#[derive(Debug, Template)] +#[template(path = "sign_in/form.html")] +struct SignInForm { + not_found: bool, + email: String, + session: Option, + page: Page, +} + +#[get("/sign-in")] +async fn render_sign_in() -> SignInForm { + SignInForm { + not_found: false, + email: "".into(), + session: None, + page: Page::SignIn, + } +} + +#[post("/sign-in")] +async fn sign_in( + req: HttpRequest, + admins: Data, + payload: Form, + admin: Option, +) -> HttpResponse { + let payload = payload.into_inner(); + let is_admin = admins + .as_ref() + .iter() + .any(|admin| admin.email == payload.email && admin.pass == payload.password); + if is_admin { + tracing::info!("Valid credentials"); + // let res = session + // .insert(SESSION_KEY, payload.email.clone()) + // .inspect_err(|e| tracing::error!("Failed to save session: {e}")); + // tracing::debug!("Saving session res: {res:?}"); + let _s = + Identity::login(&req.extensions(), payload.email).expect("Failed to store session"); + + HttpResponse::SeeOther() + .append_header(("location", "/")) + .finish() + } else { + tracing::warn!("Invalid credentials"); + HttpResponse::BadRequest() + .append_header(("Content-Type", "text/html")) + .body( + SignInForm { + email: payload.email, + not_found: true, + session: admin.and_then(|s| s.id().ok()), + page: Page::SignIn, + } + .render() + .unwrap_or_default(), + ) + } +} + +#[derive(Debug, serde::Deserialize)] +struct CreateRecipe { + // #[serde(default)] + // #[serde(with = "humantime_serde")] + // time: Option, +} + +#[derive(Debug, serde::Deserialize)] +struct SignIn { + email: String, + password: String, +} + +#[derive(Debug, Deserialize)] +struct Padding { + page: Option, +} + +#[get("/search")] +async fn search_page(admin: Option) -> SearchTemplate { + SearchTemplate { + query: "".into(), + recipies: Vec::new(), + count: 0, + session: admin.and_then(|a| a.id().ok()), + page: Page::Search, + } +} + +#[derive(Debug, Deserialize)] +struct SearchQuery { + q: String, +} + +#[post("/search")] +async fn search_results( + q: actix_web::web::Query, + admin: Option, +) -> SearchTemplate { + let query = q.into_inner().q; + SearchTemplate { + query: "".into(), + recipies: Vec::new(), + count: 0, + session: admin.and_then(|a| a.id().ok()), + page: Page::Search, + } +} + +#[get("/")] +async fn index_html( + db: Data, + q: actix_web::web::Query, + admin: Option, +) -> IndexTemplate { + let count = (entities::prelude::Recipies::find() + .count(&**db) + .await + .unwrap_or_default() as f64 + / 20.0) + .ceil() as u64; + let recipies = entities::prelude::Recipies::find() + .limit(20) + .offset(q.page.unwrap_or_default() as u64) + .all(&**db) + .await + .unwrap_or_default(); + + IndexTemplate { + recipies: recipies.into_iter().map(RecipeCard).collect(), + count, + session: admin.and_then(|s| s.id().ok()), + page: Page::Index, + } +} + +#[derive(Debug, Template)] +#[template(path = "recipies/show.html")] +struct RecipeDetailTemplate { + recipe: entities::recipies::Model, + tags: Vec, + steps: Vec, + ingeredients: Vec, + session: Option, + page: Page, +} + +#[get("/recipe/{id}")] +async fn show( + id: Path, + db: Data, + admin: Option, +) -> impl Responder { + let id = id.into_inner(); + let db = &**db; + let Ok(Some(recipe)) = entities::prelude::Recipies::find() + .filter(entities::recipies::Column::Id.eq(id)) + .one(db) + .await + else { + return HttpResponse::SeeOther() + .append_header(("location", "/")) + .finish(); + }; + let tags = entities::prelude::RecipeTags::find() + .filter(entities::recipe_tags::Column::RecipeId.eq(id)) + .all(db) + .await + .unwrap_or_default(); + let steps = entities::prelude::RecipeSteps::find() + .filter(entities::recipe_steps::Column::RecipeId.eq(id)) + .all(db) + .await + .unwrap_or_default(); + let ingeredients = entities::prelude::RecipeIngeredients::find() + .filter(entities::recipe_ingeredients::Column::RecipeId.eq(id)) + .all(db) + .await + .unwrap_or_default(); + + HttpResponse::Ok().body( + RecipeDetailTemplate { + steps, + tags, + recipe, + ingeredients, + session: admin.and_then(|s| s.id().ok()), + page: Page::Recipe, + } + .render() + .unwrap_or_default(), + ) +} + +#[get("/styles.css")] +async fn styles_css() -> HttpResponse { + HttpResponse::Ok() + .append_header(("Content-Type", "text/css")) + .body(include_str!("../assets/styles.css")) +} + +pub fn configure(config: &mut actix_web::web::ServiceConfig) { + config + .service(styles_css) + .service(render_sign_in) + .service(sign_in) + .service(index_html) + .service(show) + .service(search_page) + .service(search_results); +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5e8cee7 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +pub type User = String; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum Page { + Index, + Recipe, + Search, + SignIn, +} + +#[derive(Debug)] +pub struct Admin { + pub email: String, + pub pass: String, +} + +impl FromStr for Admin { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut it = s.split(':'); + + Ok(Self { + email: it.next().expect("Admin login is required").into(), + pass: it.next().expect("Admin password is required").into(), + }) + } +} + +#[derive(Debug, derive_more::Deref)] +pub struct Admins(Vec); + +impl FromStr for Admins { + type Err = (); + fn from_str(s: &str) -> Result { + Ok(Self( + s.trim().split(',').filter_map(|s| s.parse().ok()).collect(), + )) + } +} diff --git a/templates/index.html b/templates/index.html index 1802514..36aedf4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -23,10 +23,6 @@ {% endfor %} {% endmatch %} - -
-
Load More
-
{% endblock %} diff --git a/templates/nav.html b/templates/nav.html index 020a6b4..78dee99 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -1,7 +1,7 @@ {% block navigation %}
-
-