Files
kjvstudy.org/kjvstudy_org/utils/search.py
T
kennethreitz 83df2a37ed Refactor: Split server.py into modular packages
- 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>
2025-11-24 18:59:35 -05:00

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