more improvements and bugfixing
This commit is contained in:
@@ -113,6 +113,10 @@ for group in theological_synonym_groups:
|
||||
# Map each word to other words in its group
|
||||
theological_synonyms_map[word] = [w for w in group if w != word]
|
||||
|
||||
# Load concept mappings for common theological terms not in KJV
|
||||
concept_mappings = json.load(open("data/concept_mappings.json", "r"))
|
||||
concept_mappings_dict = {mapping["query"].lower(): mapping for mapping in concept_mappings}
|
||||
|
||||
cached: dict = {
|
||||
"verse_ids": [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()],
|
||||
"book_names": [
|
||||
@@ -123,6 +127,7 @@ cached: dict = {
|
||||
"json_dictionary": json.load(open("data/1828_Webster_KJV.json", "r")),
|
||||
"grammatical_variants": grammatical_variants_map,
|
||||
"theological_synonyms": theological_synonyms_map,
|
||||
"concept_mappings": concept_mappings_dict,
|
||||
"html_response": open("html/response.html", "r").read(),
|
||||
"html_navbar": open("html/navbar.html", "r").read(),
|
||||
"css_navbar": open("styles/header.css", "r").read(),
|
||||
@@ -470,34 +475,55 @@ def parse_query(query_override: str = None):
|
||||
return send_file("html/find.html")
|
||||
if query.endswith(","):
|
||||
query = query[:-1]
|
||||
result = show(kw=query, human_readable=human_readable)
|
||||
if not hasattr(result, "__len__"):
|
||||
logging.debug(f"parse_query.show = '{result}'")
|
||||
return result
|
||||
|
||||
if len(result) > 17:
|
||||
return result
|
||||
result = search()
|
||||
# Check if query contains both letters and digits (required for reference parsing)
|
||||
# If only numbers or only letters, skip reference parsing and go to keyword search
|
||||
has_letters = any(char.isalpha() for char in query)
|
||||
has_digits = any(char.isdigit() for char in query)
|
||||
|
||||
if not (has_letters and has_digits):
|
||||
result = search()
|
||||
else:
|
||||
result = show(kw=query, human_readable=human_readable)
|
||||
if not hasattr(result, "__len__"):
|
||||
logging.debug(f"parse_query.show = '{result}'")
|
||||
return result
|
||||
|
||||
if len(result) > 17:
|
||||
return result
|
||||
# Reference parsing failed, fall back to keyword search
|
||||
result = search()
|
||||
# Remove the closing </div> from search() so we can add dictionary content inside .responses
|
||||
if result.endswith("</div>"):
|
||||
result = result[:-6] # Remove last </div>
|
||||
result += "<style>" + cached["css_collapsible"] + "</style>"
|
||||
result += "<div class='dictionary-section'><h3>1828 Webster Definitions</h3><p>Click on the word to get the definition.</p></div>"
|
||||
|
||||
# Check if we have any dictionary definitions before showing the section
|
||||
split_query = query.replace(",", " ").split(" ")
|
||||
if len(split_query) > 20:
|
||||
return "Your Query is too long. 20 Words or less."
|
||||
|
||||
definitions_html = ""
|
||||
has_definitions = False
|
||||
for word in split_query:
|
||||
definition = define(word)
|
||||
if not definition:
|
||||
continue
|
||||
result += (
|
||||
has_definitions = True
|
||||
definitions_html += (
|
||||
"<button type='button' class='collapsible'>"
|
||||
+ word.capitalize()
|
||||
+ "</button>"
|
||||
)
|
||||
result += "<div class='definition'>" + definition + "</div>"
|
||||
definitions_html += "<div class='definition'>" + definition + "</div>"
|
||||
logging.debug(f"parse_query.define = '{word}'")
|
||||
result += "<script src='/js/collapsible.js'></script>"
|
||||
|
||||
# Only add the dictionary section if we found at least one definition
|
||||
if has_definitions:
|
||||
result += "<style>" + cached["css_collapsible"] + "</style>"
|
||||
result += "<div class='dictionary-section'><h3>1828 Webster Definitions</h3><p>Click on the word to get the definition.</p></div>"
|
||||
result += definitions_html
|
||||
result += "<script src='/js/collapsible.js'></script>"
|
||||
|
||||
result += "</div>"
|
||||
return result
|
||||
|
||||
@@ -663,7 +689,7 @@ def response_rich(scripture):
|
||||
|
||||
def scripture_response(scriptures, human_readable=False):
|
||||
response = []
|
||||
arg_view = request.args.get("view", "json")
|
||||
arg_view = request.args.get("view", "rich")
|
||||
logging.debug(f"search ->requested_format = {arg_view}")
|
||||
if arg_view == "plain":
|
||||
for scripture in scriptures:
|
||||
@@ -789,7 +815,88 @@ def search():
|
||||
return send_file("html/search.html")
|
||||
else:
|
||||
return send_file("html/search.html")
|
||||
|
||||
# Check if this is a common theological concept not in KJV
|
||||
concept_mapping = cached["concept_mappings"].get(cleaned_keywords.lower(), None)
|
||||
concept_notice = None
|
||||
if concept_mapping:
|
||||
# Build hyperlinks for all search terms
|
||||
original_query = cleaned_keywords
|
||||
search_links = []
|
||||
for term in concept_mapping["search_terms"]:
|
||||
search_link = f"/search?kw={term.replace(' ', '+')}&view=rich"
|
||||
search_links.append(f"<a href='{search_link}'><strong>{term}</strong></a>")
|
||||
|
||||
links_html = ", ".join(search_links)
|
||||
concept_notice = f"<div class='correction-notice'><strong>{original_query.capitalize()}</strong>: {concept_mapping['explanation']}<br>You may find what you are looking for with these search terms: {links_html}</div>"
|
||||
|
||||
# Use the first search term for the actual search
|
||||
cleaned_keywords = concept_mapping["search_terms"][0]
|
||||
|
||||
def score_result_by_proximity(verse_text, query_words):
|
||||
"""
|
||||
Score a verse based on how close together the query words appear.
|
||||
Higher score = words appear closer together.
|
||||
"""
|
||||
verse_lower = verse_text.lower()
|
||||
|
||||
# Find all positions of each query word
|
||||
word_positions = {}
|
||||
for word in query_words:
|
||||
word_lower = word.lower()
|
||||
positions = []
|
||||
start = 0
|
||||
while True:
|
||||
pos = verse_lower.find(word_lower, start)
|
||||
if pos == -1:
|
||||
break
|
||||
positions.append(pos)
|
||||
start = pos + 1
|
||||
word_positions[word] = positions
|
||||
|
||||
# If any word is missing, score is 0
|
||||
if any(len(positions) == 0 for positions in word_positions.values()):
|
||||
return 0
|
||||
|
||||
# Calculate minimum distance between all query words
|
||||
# For each combination of positions, find the span (max_pos - min_pos)
|
||||
# Smaller span = higher score
|
||||
import itertools
|
||||
|
||||
# Get all combinations of positions for all words
|
||||
all_position_combos = list(itertools.product(*word_positions.values()))
|
||||
|
||||
if not all_position_combos:
|
||||
return 0
|
||||
|
||||
# Find the combination with minimum span
|
||||
min_span = float('inf')
|
||||
for combo in all_position_combos:
|
||||
span = max(combo) - min(combo)
|
||||
if span < min_span:
|
||||
min_span = span
|
||||
|
||||
# Convert span to score (smaller span = higher score)
|
||||
# Use inverse: 1000 / (span + 1) so adjacent words score ~1000, far apart score low
|
||||
score = 1000.0 / (min_span + 1)
|
||||
|
||||
return score
|
||||
|
||||
results = lookup_fts(cleaned_keywords)
|
||||
|
||||
# Sort results by proximity score if we have multiple query words
|
||||
if results and len(cleaned_keywords.split()) > 1:
|
||||
query_words = cleaned_keywords.split()
|
||||
scored_results = []
|
||||
for result in results:
|
||||
verse_text = result[4] # The text is at index 4
|
||||
score = score_result_by_proximity(verse_text, query_words)
|
||||
scored_results.append((score, result))
|
||||
|
||||
# Sort by score descending, then keep just the results
|
||||
scored_results.sort(key=lambda x: x[0], reverse=True)
|
||||
results = [result for score, result in scored_results]
|
||||
|
||||
if not results:
|
||||
# PRIORITY 1: Try grammatical variant expansion first
|
||||
words = cleaned_keywords.split()
|
||||
@@ -904,9 +1011,45 @@ def search():
|
||||
|
||||
# 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"
|
||||
)
|
||||
arg_view = request.args.get("view", "rich")
|
||||
|
||||
if arg_view == "rich":
|
||||
# Build a nice no-results page with header
|
||||
no_results_html = cached["html_navbar"].format(
|
||||
query=cleaned_keywords,
|
||||
current_chapter_name="",
|
||||
next_chapter_name="",
|
||||
next_chapter_link="",
|
||||
prev_chapter_name="",
|
||||
prev_chapter_link=""
|
||||
)
|
||||
no_results_html += cached["css_navbar"]
|
||||
no_results_html += "<div class='responses'>"
|
||||
no_results_html += f"<div class='correction-notice'><strong>No results found</strong> for '{cleaned_keywords}'.</div>"
|
||||
|
||||
if suggestions:
|
||||
no_results_html += "<div class='theological-suggestions'><h3>Did you mean?</h3>"
|
||||
no_results_html += "<p>We couldn't find any verses matching your search. Here are some spelling suggestions:</p>"
|
||||
for suggestion in suggestions[:5]: # Limit to 5 suggestions
|
||||
search_link = f"/search?kw={suggestion}&view=rich"
|
||||
no_results_html += f"<div class='suggestion-item'><a href='{search_link}'><strong>{suggestion}</strong></a></div>"
|
||||
no_results_html += "</div>"
|
||||
else:
|
||||
no_results_html += "<div class='theological-suggestions'><h3>Search Tips</h3>"
|
||||
no_results_html += "<p>We couldn't find any verses matching your search. Try:</p>"
|
||||
no_results_html += "<ul style='font-family: var(--font-ui); color: var(--color-text-secondary);'>"
|
||||
no_results_html += "<li>Checking your spelling</li>"
|
||||
no_results_html += "<li>Using different keywords</li>"
|
||||
no_results_html += "<li>Searching for <a href='/search?kw=love&view=rich'>love</a>, <a href='/search?kw=faith&view=rich'>faith</a>, or <a href='/search?kw=hope&view=rich'>hope</a></li>"
|
||||
no_results_html += "</ul></div>"
|
||||
|
||||
no_results_html += "</div>"
|
||||
return no_results_html
|
||||
else:
|
||||
# Plain text fallback
|
||||
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
|
||||
@@ -918,15 +1061,27 @@ def search():
|
||||
for synonym in synonyms:
|
||||
theological_suggestions.append(synonym)
|
||||
|
||||
arg_view = request.args.get("view", None)
|
||||
arg_view = request.args.get("view", "rich")
|
||||
if arg_view == "plain":
|
||||
response = "<br>".join(response_list)
|
||||
if concept_notice:
|
||||
response = concept_notice + "<br>" + response
|
||||
if theological_suggestions:
|
||||
response += "<br><br>Also consider searching: "
|
||||
for suggestion in theological_suggestions:
|
||||
response += f"{suggestion}<br>"
|
||||
elif arg_view == "rich":
|
||||
response = "".join(response_list)
|
||||
# Insert concept notice at the top if it exists
|
||||
if concept_notice:
|
||||
# Find the first </div> after the opening <div class='responses'> and insert after it
|
||||
responses_start = response.find("<div class='responses'>")
|
||||
if responses_start != -1:
|
||||
# Find the results-count div closing
|
||||
insert_pos = response.find("</div>", responses_start + len("<div class='responses'>"))
|
||||
if insert_pos != -1:
|
||||
insert_pos += len("</div>")
|
||||
response = response[:insert_pos] + concept_notice + response[insert_pos:]
|
||||
if theological_suggestions:
|
||||
# Add suggestions before the LAST closing </div> which closes the responses container
|
||||
suggestions_html = "<div class='theological-suggestions'><h3>Related Theological Concepts</h3>"
|
||||
|
||||
Reference in New Issue
Block a user