Skip to main content

terraphim_middleware/thesaurus/
mod.rs

1//! Logseq is a knowledge graph that uses Markdown files to store notes. This
2//! module provides a middleware for creating a Thesaurus from a Logseq
3//! haystack.
4//!
5//! Example:
6//!
7//! If we parse a file named `path/to/concept.md` with the following content:
8//!
9//! ```markdown
10//! synonyms:: foo, bar, baz
11//! ```
12//!
13//! Then the thesaurus will contain the following entries:
14//!
15//! ```rust
16//! use terraphim_types::{Thesaurus, Concept, NormalizedTerm};
17//! let concept = Concept::new("concept".into());
18//! let nterm = NormalizedTerm::new(concept.id, concept.value.clone());
19//! let mut thesaurus = Thesaurus::new("Engineer".to_string());
20//! thesaurus.insert(concept.value.clone(), nterm.clone());
21//! thesaurus.insert("foo".to_string().into(),nterm.clone());
22//! thesaurus.insert("bar".to_string().to_string().into(), nterm.clone());
23//! thesaurus.insert("baz".to_string().into(), nterm.clone());
24//! ```
25//! The logic as follows: if you ask for concept by name you get concept, if you ask (get) for any of the synonyms you will get concept with id,
26//! its pre-computed reverse tree traversal - any of the synonyms (leaf) maps into the concepts (root)
27
28pub use terraphim_automata::builder::{Logseq, ThesaurusBuilder};
29use terraphim_config::ConfigState;
30use terraphim_persistence::Persistable;
31use terraphim_rolegraph::{RoleGraph, RoleGraphSync};
32use terraphim_types::SearchQuery;
33use terraphim_types::{RoleName, Thesaurus};
34
35use crate::Result;
36use std::path::PathBuf;
37
38pub async fn build_thesaurus_from_haystack(
39    config_state: &mut ConfigState,
40    search_query: &SearchQuery,
41) -> Result<()> {
42    // build thesaurus from haystack or load from remote
43    // FIXME: introduce LRU cache for locally build thesaurus via persistance crate
44    log::debug!("Building thesaurus from haystack");
45    let config = config_state.config.lock().await.clone();
46    let roles = config.roles.clone();
47    let default_role = config.default_role.clone();
48    let role_name = search_query.role.clone().unwrap_or_default();
49    log::debug!("Role name: {}", role_name);
50    let role = roles
51        .get(&role_name)
52        .or_else(|| roles.get(&default_role))
53        .or_else(|| roles.values().next())
54        .ok_or_else(|| {
55            crate::Error::RoleNotFound(format!(
56                "No role found: requested='{}', default='{}', available={:?}",
57                role_name,
58                default_role,
59                roles
60                    .keys()
61                    .map(|k| k.original.as_str())
62                    .collect::<Vec<_>>()
63            ))
64        })?
65        .to_owned();
66    log::debug!("Role: {:?}", role);
67    for haystack in &role.haystacks {
68        log::debug!("Updating thesaurus for haystack: {:?}", haystack);
69
70        let logseq = Logseq::default();
71        let mut thesaurus: Thesaurus = logseq
72            .build(
73                role_name.as_lowercase().to_string(),
74                PathBuf::from(&haystack.location),
75            )
76            .await?;
77        match thesaurus.save().await {
78            Ok(_) => {
79                log::info!("Thesaurus for role `{}` saved to persistence", role_name);
80                // We reload the thesaurus from persistence to ensure we are using the
81                // canonical, persisted version going forward.
82                thesaurus = thesaurus.load().await?;
83            }
84            Err(e) => log::error!("Failed to save thesaurus: {:?}", e),
85        }
86
87        log::debug!("Make sure thesaurus updated in a role {}", role_name);
88
89        update_thesaurus(config_state, &role_name, thesaurus).await?;
90    }
91    Ok(())
92}
93
94async fn update_thesaurus(
95    config_state: &mut ConfigState,
96    role_name: &RoleName,
97    thesaurus: Thesaurus,
98) -> Result<()> {
99    log::debug!("Updating thesaurus for role: {}", role_name);
100    let rolegraph = RoleGraph::new(role_name.clone(), thesaurus).await;
101    match rolegraph {
102        Ok(rolegraph) => {
103            let rolegraph_value = RoleGraphSync::from(rolegraph);
104            // Actually update the config_state.roles, not just a local copy
105            config_state
106                .roles
107                .insert(role_name.clone(), rolegraph_value);
108            log::info!("Successfully updated rolegraph for role: {}", role_name);
109        }
110        Err(e) => log::error!("Failed to update role and thesaurus: {:?}", e),
111    }
112
113    Ok(())
114}