knowledge_base_crud/read/entities/
search.rs1use super::{Entities, query::load_entities};
2use crate::Error;
3use knowledge_base_models::Entity;
4use serde::Serialize;
5
6#[derive(Clone, Debug, Serialize)]
7pub struct EntitySearchPage {
8 pub query: String,
9 pub offset: usize,
10 pub limit: usize,
11 pub total: usize,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub next_offset: Option<usize>,
14 pub entities: Vec<Entity>,
15}
16
17impl Entities<'_> {
18 pub fn search(&self, query: &str, limit: usize, offset: usize) -> Result<EntitySearchPage, Error> {
19 let query = normalize(query);
20 if query.is_empty() {
21 return Err(Error::InvalidRequest("entity search query must not be empty".to_owned()));
22 }
23 if limit == 0 {
24 return Err(Error::InvalidRequest("entity search limit must be greater than zero".to_owned()));
25 }
26
27 let mut entities = load_entities(self.repository.root())?
28 .into_iter()
29 .filter(|entity| entity.labels.values().any(|label| normalize(&label.text).contains(&query)))
30 .collect::<Vec<_>>();
31 entities.sort_by(|left, right| {
32 has_exact_label(left, &query)
33 .cmp(&has_exact_label(right, &query))
34 .reverse()
35 .then_with(|| left.id.number().cmp(&right.id.number()))
36 });
37
38 let total = entities.len();
39 let entities = entities.into_iter().skip(offset).take(limit).collect::<Vec<_>>();
40 let returned_end = offset.saturating_add(entities.len());
41 let next_offset = (returned_end < total).then_some(returned_end);
42
43 Ok(EntitySearchPage {
44 query,
45 offset,
46 limit,
47 total,
48 next_offset,
49 entities,
50 })
51 }
52}
53
54fn normalize(value: &str) -> String {
55 value.trim().to_lowercase()
56}
57
58fn has_exact_label(entity: &Entity, query: &str) -> bool {
59 entity.labels.values().any(|label| normalize(&label.text) == query)
60}