mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
83df2a37ed
- Create kjvstudy_org/utils/ package: - books.py: Book name normalization, abbreviations, testament categorization - helpers.py: Utility functions (create_slug, get_verse_text, etc.) - search.py: Full-text search with FTS5 support - search_index.py: SQLite FTS5 search index implementation - Create kjvstudy_org/routes/ package: - api.py: All /api/* endpoints as FastAPI router - Update server.py: - Import from new modular packages - Include API router via app.include_router() - Remove duplicate API routes - Fix test_edge_cases.py to accept 404 for reversed verse ranges All 100 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
102 lines
3.1 KiB
Python
102 lines
3.1 KiB
Python
"""Search functionality for Bible verses."""
|
|
from typing import List, Dict, Optional
|
|
|
|
from ..kjv import bible
|
|
from .helpers import is_verse_reference, parse_verse_reference
|
|
|
|
# Try to use FTS5 search if available
|
|
_use_fts = False
|
|
try:
|
|
from .search_index import search_verses as fts_search, init_search_index, DB_PATH
|
|
_use_fts = True
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
def perform_full_text_search(query: str, limit: Optional[int] = None) -> List[Dict]:
|
|
"""
|
|
Perform full text search across all Bible verses.
|
|
|
|
Uses SQLite FTS5 for efficient searching when available,
|
|
falls back to O(n) iteration otherwise.
|
|
"""
|
|
# First, check if this looks like a verse reference
|
|
if is_verse_reference(query):
|
|
verse_result = parse_verse_reference(query)
|
|
if verse_result:
|
|
return [verse_result]
|
|
|
|
# Use FTS5 if available and index exists
|
|
if _use_fts and DB_PATH.exists():
|
|
return fts_search(query, limit)
|
|
|
|
# Fallback to original O(n) search
|
|
return _legacy_search(query, limit)
|
|
|
|
|
|
def _legacy_search(query: str, limit: Optional[int] = None) -> List[Dict]:
|
|
"""Original O(n) search through all verses - used as fallback."""
|
|
results = []
|
|
search_terms = query.lower().split()
|
|
|
|
for verse in bible.iter_verses():
|
|
verse_text = verse.text.lower()
|
|
|
|
# Check if all search terms are in the verse
|
|
if all(term in verse_text for term in search_terms):
|
|
score = calculate_relevance_score(verse.text, search_terms)
|
|
|
|
results.append({
|
|
"book": verse.book,
|
|
"chapter": verse.chapter,
|
|
"verse": verse.verse,
|
|
"text": verse.text,
|
|
"reference": f"{verse.book} {verse.chapter}:{verse.verse}",
|
|
"url": f"/book/{verse.book}/chapter/{verse.chapter}#verse-{verse.verse}",
|
|
"score": score,
|
|
"highlighted_text": highlight_search_terms(verse.text, search_terms)
|
|
})
|
|
|
|
# Sort by relevance score (higher is better)
|
|
results.sort(key=lambda x: x["score"], reverse=True)
|
|
|
|
if limit is not None:
|
|
return results[:limit]
|
|
return results
|
|
|
|
|
|
def calculate_relevance_score(text: str, search_terms: List[str]) -> float:
|
|
"""Calculate relevance score for search results."""
|
|
text_lower = text.lower()
|
|
score = 0.0
|
|
|
|
for term in search_terms:
|
|
# Count occurrences of each term
|
|
count = text_lower.count(term.lower())
|
|
score += count
|
|
|
|
# Bonus for exact word matches
|
|
if f" {term.lower()} " in f" {text_lower} ":
|
|
score += 0.5
|
|
|
|
return score
|
|
|
|
|
|
def highlight_search_terms(text: str, search_terms: List[str]) -> str:
|
|
"""Highlight search terms in text."""
|
|
import re
|
|
highlighted = text
|
|
for term in search_terms:
|
|
# Case-insensitive replacement
|
|
pattern = re.compile(re.escape(term), re.IGNORECASE)
|
|
highlighted = pattern.sub(f'<mark>{term}</mark>', highlighted)
|
|
return highlighted
|
|
|
|
|
|
def ensure_search_index():
|
|
"""Ensure the FTS5 search index is built."""
|
|
if _use_fts:
|
|
init_search_index()
|
|
return True
|
|
return False
|