This commit is contained in:
Adrian Woźniak 2024-10-28 14:59:33 +01:00
parent 2849d9f339
commit 6d487dd5c7
12 changed files with 879 additions and 313 deletions

376
Cargo.lock generated
View File

@ -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",
]

View File

@ -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]

View File

@ -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;
}

1
src/actors/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod search;

129
src/actors/search.rs Normal file
View File

@ -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<Mutex<Inner>>);
impl SearchEngine {
pub fn build() -> Result<Self, tantivy::TantivyError> {
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<Self>;
}
#[derive(Debug, Message)]
#[rtype(result = "Result<u64,TantivyError>")]
pub struct CreateRecipe {
id: u64,
title: String,
summary: String,
}
impl Handler<CreateRecipe> for SearchEngine {
type Result = actix::ResponseActFuture<Self, Result<u64, TantivyError>>;
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<Vec<u64>,TantivyError>")]
pub struct Find {
query: String,
}
impl Handler<Find> for SearchEngine {
type Result = actix::ResponseActFuture<Self, Result<Vec<u64>, 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<TantivyDocument> = 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),
)
}
}

17
src/filters.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::types::Page;
pub fn duration(sec: &&i32) -> ::askama::Result<String> {
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<String> {
tracing::info!("page_class: {pair:?}");
Ok(match pair {
(current, expected) if current == expected => " border-r-4 border-primary ",
_ => "",
}
.into())
}

View File

@ -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<String> {
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<RecipeCard>,
count: u64,
session: Option<User>,
page: Page,
}
#[derive(Debug, Template)]
#[template(path = "top_bar.html")]
struct TopBar<'s> {
session: &'s Option<User>,
}
impl<'s> TopBar<'s> {
pub fn new(session: &'s Option<User>) -> 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<Self, Self::Err> {
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<Admin>);
impl FromStr for Admins {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<chrono::Duration>,
}
#[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<User>,
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<Admins>,
payload: Form<SignIn>,
admin: Option<Identity>,
) -> 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<u16>,
}
#[get("/")]
async fn index_html(
db: Data<DatabaseConnection>,
q: actix_web::web::Query<Padding>,
admin: Option<Identity>,
) -> 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<entities::recipe_tags::Model>,
steps: Vec<entities::recipe_steps::Model>,
ingeredients: Vec<entities::recipe_ingeredients::Model>,
session: Option<User>,
page: Page,
}
#[get("/recipe/{id}")]
async fn show(
id: Path<i64>,
db: Data<DatabaseConnection>,
admin: Option<Identity>,
) -> 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")

256
src/routes.rs Normal file
View File

@ -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<RecipeCard>,
count: u64,
session: Option<User>,
page: Page,
}
#[derive(Debug, Template)]
#[template(path = "search.html")]
struct SearchTemplate {
recipies: Vec<RecipeCard>,
count: u64,
query: String,
session: Option<User>,
page: Page,
}
#[derive(Debug, Template)]
#[template(path = "top_bar.html")]
struct TopBar<'s> {
session: &'s Option<User>,
}
impl<'s> TopBar<'s> {
pub fn new(session: &'s Option<User>) -> 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<User>,
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<Admins>,
payload: Form<SignIn>,
admin: Option<Identity>,
) -> 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<chrono::Duration>,
}
#[derive(Debug, serde::Deserialize)]
struct SignIn {
email: String,
password: String,
}
#[derive(Debug, Deserialize)]
struct Padding {
page: Option<u16>,
}
#[get("/search")]
async fn search_page(admin: Option<Identity>) -> 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<SearchQuery>,
admin: Option<Identity>,
) -> 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<DatabaseConnection>,
q: actix_web::web::Query<Padding>,
admin: Option<Identity>,
) -> 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<entities::recipe_tags::Model>,
steps: Vec<entities::recipe_steps::Model>,
ingeredients: Vec<entities::recipe_ingeredients::Model>,
session: Option<User>,
page: Page,
}
#[get("/recipe/{id}")]
async fn show(
id: Path<i64>,
db: Data<DatabaseConnection>,
admin: Option<Identity>,
) -> 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);
}

42
src/types.rs Normal file
View File

@ -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<Self, Self::Err> {
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<Admin>);
impl FromStr for Admins {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
))
}
}

View File

@ -23,10 +23,6 @@
{% endfor %}
{% endmatch %}
</div>
<div class="flex justify-center">
<div class="btn bg-secondary-100 text-secondary-200 hover:shadow-inner transform hover:scale-110 hover:bg-opacity-60 transition ease-out duration-300">Load More</div>
</div>
</div>
</main>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% block navigation %}
<!-- navigation -->
<div class="md:col-span-1 md:flex md:justify-end">
<nav class="text-right">
<nav class="text-right flex flex-col gap-8">
<div class="flex justify-between items-center">
<h1 class="font-bold uppercase p-4 border-b border-gray-100">
<a href="/" class="hover:text-gray-600">Cooked</a>
@ -12,9 +12,9 @@
</svg>
</div>
</div>
<ul class="text-sm mt-6 hidden md:block" id="menu">
<li class="text-gray-700 font-bold py-2">
<a href="/" class="px-4 flex justify-end border-r-4 border-primary hover:shadow-md">
<ul class="text-sm hidden md:flex flex-col gap-8" id="menu">
<li>
<a href="/" class="px-4 flex justify-end hover:shadow-md {{(page, Page::Index)|page_class}}">
<span>Home</span>
<svg class="w-5 ml-2" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="/search" class="px-4 flex justify-end border-r-4 border-primary hover:shadow-md">
<a href="/search" class="px-4 flex justify-end hover:shadow-md {{(page, Page::Search)|page_class}}">
<span>Search</span>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 ml-2">
<rect width="24" height="24" fill="white"/>

35
templates/search.html Normal file
View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<main class="md:col-span-3 flex flex-col gap-4 m-4">
{{ TopBar::new(session)|safe }}
<header class="flex flex-col gap-2">
<h2 class="text-gray-600 text-6xl font-semibold text-center">Search</h2>
</header>
<div class="flex flex-col gap-8">
<div class="flex flex-wrap gap-8 justify-center">
<form action="/search" method="post">
<input name="q" class="input input-search" type="search" />
</form>
</div>
</div>
<div class="flex flex-col gap-8">
<div class="flex flex-wrap gap-8 justify-center">
{% for recipe in recipies %}
{{ recipe|safe }}
{% endfor %}
</div>
<div class="flex gap-4 flex-wrap">
{% match count %}
{% when 0 %}
{% when _ %}
{% for page in 0..count %}
<a class="btn" href="/?page={{page}}">{{page}}</a>
{% endfor %}
{% endmatch %}
</div>
</div>
</main>
{% endblock %}