diff --git a/app.py b/app.py index dd28fcc..0a55bbb 100644 --- a/app.py +++ b/app.py @@ -97,6 +97,22 @@ if not kjv_cur: if not sermon_cur: print("WARN: Error connecting to Sermon Database") +# Build grammatical variants lookup from word groups +variant_groups = json.load(open("data/grammatical_variants.json", "r")) +grammatical_variants_map = {} +for group in variant_groups: + for word in group: + # Map each word to all other words in its group + grammatical_variants_map[word] = [w for w in group if w != word] + +# Build theological synonyms lookup from word groups +theological_synonym_groups = json.load(open("data/theological_synonyms.json", "r")) +theological_synonyms_map = {} +for group in theological_synonym_groups: + for word in group: + # Map each word to other words in its group + theological_synonyms_map[word] = [w for w in group if w != word] + cached: dict = { "verse_ids": [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()], "book_names": [ @@ -105,7 +121,8 @@ cached: dict = { "chapter_list": [], "books_chapter_lengths": {}, "json_dictionary": json.load(open("data/1828_Webster_KJV.json", "r")), - "grammatical_variants": json.load(open("data/grammatical_variants.json", "r")), + "grammatical_variants": grammatical_variants_map, + "theological_synonyms": theological_synonyms_map, "html_response": open("html/response.html", "r").read(), "html_navbar": open("html/navbar.html", "r").read(), "css_navbar": open("styles/header.css", "r").read(), @@ -777,11 +794,43 @@ def search(): # PRIORITY 1: Try grammatical variant expansion first words = cleaned_keywords.split() + def sort_variants_by_similarity(original, variants): + """Sort variants by morphological similarity: forwards (longer/more specific) then backwards (shorter/simpler)""" + def get_common_prefix_length(w1, w2): + """Find how many characters match from the start""" + prefix_len = 0 + for i in range(min(len(w1), len(w2))): + if w1[i].lower() == w2[i].lower(): + prefix_len += 1 + else: + break + return prefix_len + + scored_variants = [] + for variant in variants: + prefix_len = get_common_prefix_length(original, variant) + + # Reject variants that don't share the stem (beginning doesn't match) + if prefix_len < min(2, len(original) // 2): + continue + + # Score: prioritize common prefix length (stem preservation) heavily, + # then prefer longer variants (forward/more specific) over shorter (backward/simpler) + # Prefix is weighted 1000x to ensure stem-preservation is primary + score = (prefix_len * 1000) + len(variant) + scored_variants.append((score, variant)) + + # Sort by score descending: highest score = most similar with longest form first + return [v for _, v in sorted(scored_variants, key=lambda x: x[0], reverse=True)] + for word in words: variants = cached["grammatical_variants"].get(word.lower(), []) if variants: - # Try each variant individually - for variant in variants: + # Sort variants: forwards in likeness (longer/specific) then backwards (shorter/simple) + sorted_variants = sort_variants_by_similarity(word.lower(), variants) + + # Try each variant in order of similarity + for variant in sorted_variants: variant_query = cleaned_keywords.replace(word, variant, 1) results = lookup_fts(variant_query) if results: @@ -832,17 +881,66 @@ def search(): response = jsonify({"corrected_from": cleaned_keywords, "corrected_to": corrected_keyword, "results": response_list}) return response - # PRIORITY 3: If we still have no results, show spelling suggestions + # PRIORITY 3: Try theological synonym expansion + for word in words: + synonyms = cached["theological_synonyms"].get(word.lower(), None) + if synonyms: + for synonym in synonyms: + synonym_query = cleaned_keywords.replace(word, synonym, 1) + results = lookup_fts(synonym_query) + if results: + response_list = scripture_response(results) + arg_view = request.args.get("view", None) + explanation = f"In the KJV, '{word}' and '{synonym}' often refer to similar theological concepts. Consider also searching for related verses." + if arg_view == "plain": + response = f"Showing results for '{synonym_query}' (theological equivalent of '{cleaned_keywords}')\n{explanation}\n
" + "
".join(response_list) + elif arg_view == "rich": + theological_notice = f"
Showing results for '{synonym_query}' (theological equivalent of '{cleaned_keywords}')
{explanation}
" + response_str = "".join(response_list) + response = response_str.replace("
", f"
{theological_notice}", 1) + else: + response = jsonify({"original": cleaned_keywords, "theological_equivalent": synonym_query, "explanation": explanation, "results": response_list}) + return response + + # PRIORITY 4: If we still have no results, show spelling suggestions suggestions = h.suggest(cleaned_keywords) return "{} not found, try one of the following: {}".format( cleaned_keywords, ", ".join(suggestions) if suggestions else "No suggestions available" ) response_list = scripture_response(results) + + # Check if any words in the search have theological synonyms to suggest + theological_suggestions = [] + words = cleaned_keywords.split() + for word in words: + synonyms = cached["theological_synonyms"].get(word.lower(), None) + if synonyms: + for synonym in synonyms: + theological_suggestions.append(synonym) + arg_view = request.args.get("view", None) if arg_view == "plain": response = "
".join(response_list) + if theological_suggestions: + response += "

Also consider searching: " + for suggestion in theological_suggestions: + response += f"{suggestion}
" elif arg_view == "rich": response = "".join(response_list) + if theological_suggestions: + # Add suggestions before the LAST closing
which closes the responses container + suggestions_html = "

Related Theological Concepts

" + suggestions_html += "

In the KJV, these terms often refer to similar theological concepts. Consider also searching for related verses.

" + for suggestion in theological_suggestions: + search_link = f"/search?kw={suggestion}&view=rich" + suggestions_html += f"
" + suggestions_html += f"{suggestion.capitalize()}" + suggestions_html += "
" + suggestions_html += "
" + # Find the last
and insert before it + last_div_pos = response.rfind("") + if last_div_pos != -1: + response = response[:last_div_pos] + suggestions_html + response[last_div_pos:] else: response = jsonify(response_list) #logging.debug(f"Search Response: {response}") diff --git a/data/grammatical_variants.json b/data/grammatical_variants.json index b307b9c..f171d69 100644 --- a/data/grammatical_variants.json +++ b/data/grammatical_variants.json @@ -1,20 +1,29 @@ -{ - "you": ["ye", "thee", "thou"], - "ye": ["you", "thee", "thou"], - "thee": ["you", "ye", "thou"], - "thou": ["you", "ye", "thee"], - "your": ["thy", "thine"], - "thy": ["your", "thine"], - "thine": ["your", "thy"], - "has": ["hath"], - "hath": ["has", "have"], - "have": ["hath"], - "does": ["doth"], - "doth": ["does", "do"], - "do": ["doth"], - "says": ["saith"], - "saith": ["says", "said"], - "said": ["saith"], - "are": ["art"], - "art": ["are"] -} +[ + ["you", "ye", "thee", "thou"], + ["your", "thy", "thine"], + ["have", "has", "hath", "hast"], + ["do", "does", "doth", "doest"], + ["say", "says", "said", "saith"], + ["are", "art"], + ["was", "were", "wast", "wert"], + ["will", "wilt", "shall", "shalt"], + ["go", "goes", "goeth", "goest"], + ["know", "knows", "knoweth", "knowest"], + ["see", "sees", "seeth", "seest"], + ["give", "gives", "giveth", "givest"], + ["make", "makes", "maketh", "makest"], + ["come", "comes", "cometh", "comest"], + ["take", "takes", "taketh", "takest"], + ["can", "canst"], + ["may", "mayest"], + ["hear", "hears", "heareth", "hearest"], + ["walk", "walks", "walketh", "walkest"], + ["speak", "speaks", "speaketh", "speakest"], + ["live", "lives", "liveth", "livest"], + ["call", "calls", "calleth", "callest"], + ["fear", "fears", "feareth", "fearest"], + ["love", "loves", "loveth", "lovest"], + ["believe", "believes", "believeth", "believest"], + ["did", "didst"], + ["eat", "eats", "eateth", "eatest"] +] diff --git a/data/theological_synonyms.json b/data/theological_synonyms.json new file mode 100644 index 0000000..bc077a0 --- /dev/null +++ b/data/theological_synonyms.json @@ -0,0 +1,7 @@ +[ + ["charity", "love"], + ["ghost", "spirit"], + ["righteousness", "holiness"], + ["salvation", "redemption"], + ["faith", "belief"] +] diff --git a/styles/header.css b/styles/header.css index 798fa02..5cee892 100644 --- a/styles/header.css +++ b/styles/header.css @@ -96,7 +96,8 @@ body { /* Correction & Variant Notices */ .correction-notice, -.variant-notice { +.variant-notice, +.theological-notice { font-family: var(--font-ui); font-size: 0.95rem; padding: var(--space-md) var(--space-lg); @@ -108,11 +109,20 @@ body { } .correction-notice strong, -.variant-notice strong { +.variant-notice strong, +.theological-notice strong { color: var(--color-accent-primary); font-weight: 600; } +.theological-notice em { + display: block; + margin-top: var(--space-sm); + color: var(--color-text-muted); + font-style: italic; + font-size: 0.9rem; +} + /* Results Count */ .results-count { font-family: var(--font-ui); @@ -122,6 +132,52 @@ body { font-weight: 500; } +/* Theological Suggestions Section */ +.theological-suggestions { + margin-top: var(--space-2xl); + padding: var(--space-lg); + background-color: var(--color-bg-secondary); + border-radius: var(--border-radius); + border: 1px solid var(--color-border); +} + +.theological-suggestions h3 { + font-family: var(--font-ui); + font-size: 1.25rem; + color: var(--color-text-primary); + margin: 0 0 var(--space-md) 0; + font-weight: 600; +} + +.suggestion-item { + font-family: var(--font-ui); + font-size: 1rem; + margin: var(--space-sm) 0; + padding: var(--space-sm) 0; + color: var(--color-text-secondary); +} + +.suggestion-item a { + color: var(--color-accent-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +.suggestion-item a:hover { + color: var(--color-accent-hover); + text-decoration: underline; +} + +.suggestion-item strong { + font-weight: 600; +} + +.suggestion-item em { + font-style: italic; + color: var(--color-text-muted); + font-size: 0.95rem; +} + /* Dictionary Section */ .dictionary-section { margin: var(--space-xl) 0 var(--space-lg) 0;