Skip to content

POC: wire search page to semantic KNN search (sc-45806) - #3589

Open
nsantacruz wants to merge 8 commits into
masterfrom
feature/sc-45806/create-a-cauldron-with-semantic-search
Open

POC: wire search page to semantic KNN search (sc-45806)#3589
nsantacruz wants to merge 8 commits into
masterfrom
feature/sc-45806/create-a-cauldron-with-semantic-search

Conversation

@nsantacruz

Copy link
Copy Markdown
Contributor

Summary

  • Disables Elasticsearch keyword search on the /search page (text search only) and instead renders results from the semantic KNN search pipeline.
  • Up to 40 direct semantic matches, followed by up to 10 link-origin matches (refs frequently linked-to from the top semantic hits), each labeled with a chip in the result title (Semantic Match / Related Link).
  • Filter sidebar and sort dropdown are hidden on semantic results — not useful for this few, non-facetable results.
  • Sheet search is untouched (still keyword/Elasticsearch) since the KNN index only covers texts.

Why a new backend endpoint?

/api/knn-search is gated by a shared Authorization: Bearer <SEMANTIC_SEARCH_API_TOKEN> secret intended for server-to-server/tool callers. Calling it directly from browser JS would ship that secret to every visitor. Instead:

  • KnnSearch.post() was split into a thin auth/dispatch wrapper and a reusable KnnSearch.run_search(body) classmethod (api/views.py).
  • A new public, same-origin view semantic_search_wrapper_api (reader/views.py, routed at POST /api/search-wrapper/semantic) calls run_search directly server-side — no token required, same trust model as the existing search_wrapper_api.

Frontend

  • New SemanticSearchQuerier.jsx (sibling to ElasticSearchQuerier.jsx) posts to the new endpoint, tags each hit resultOrigin: 'semantic'|'link', and feeds SearchPage.
  • SearchResultList.jsx gets a new type === "semantic" render branch (avoids the !!result._source.version filter, which would silently drop every link-origin result).
  • SearchTextResult.jsx renders the origin chip and falls back to a version-less href for link-origin rows (which lack version/lang).
  • SearchPage.jsx hides the sort/filter controls when type === "semantic".
  • ReaderPanel.jsx mounts SemanticSearchQuerier for text search, keeps ElasticSearchQuerier for sheet search.

Test plan

  • webpack build succeeds across all bundles.
  • Django imports cleanly; exercised /api/search-wrapper/semantic via Django test client — correctly calls Gemini to embed the query and reaches the pgvector query (failed only on local Postgres not running in this sandbox).
  • Manual: docker-compose up, open /search?q=..., confirm no filter sidebar/sort dropdown, semantic results first with chips, link results below with chips, click-through navigates correctly.
  • Manual: confirm sheet search on the search page is unaffected.

🤖 Generated with Claude Code

Disables Elasticsearch keyword search on the /search page and instead
renders results from the semantic KNN search pipeline: up to 40 direct
semantic matches followed by up to 10 link-origin matches, each tagged
with a chip. Filter sidebar and sort dropdown are hidden since they
aren't useful for this few, non-facetable results. Sheet search is
unaffected (KNN index only covers texts).

/api/knn-search is bearer-token gated for external/tool callers, so
this adds a public same-origin proxy (api/search-wrapper/semantic)
that reuses the same core search logic (KnnSearch.run_search) without
shipping the shared secret to browser JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gitvelocity-reviewer

Copy link
Copy Markdown

📊 Code Quality Score: 31/100

Base Score 51 × ESF 0.6 = 30.6 ≈ 31

Category Score Factors
🔭 Scope 13/20 9 files across backend (Python views, URL routing) and frontend (JSX, CSS); new public API endpoint; new React component; routing change affecting all non-sheet searches
🏗️ Architecture 12/20 Clean extraction of run_search() classmethod; new KnnSearchError exception type; SemanticSearchQuerier mirrors ElasticSearchQuerier pattern; bypasses existing auth pattern for same-origin calls which is an architectural risk
⚙️ Implementation 10/20 Refactoring of post() to classmethod is clean; result normalization is straightforward; conditional routing in ReaderPanel is simple but impactful; no complex algorithms, mostly plumbing and wiring
⚠️ Risk 12/20 All non-sheet text searches now route through semantic search with no feature flag; @csrf_exempt on public endpoint calling paid Gemini API with no rate limiting; deprecated React lifecycle method (componentWillReceiveProps); heRef bug indicates incomplete testing
✅ Quality 3/15 No tests added for new endpoint or classmethod refactor; POC comment in code suggests incomplete state; good docstrings on Python functions; heRef normalization bug indicates lack of testing
🔒 Perf / Security 1/5 No rate limiting on new public endpoint; Gemini API cost exposure via unauthenticated endpoint; no monitoring or alerting added

Was this score accurate? 👍 Yes · 👎 No

How this was scored →

Scored by GitVelocity · How are scores calculated?

nsantacruz and others added 7 commits August 6, 2026 14:21
SemanticSearchQuerier set isQueryRunning but left the previous query's
hits in state, so SearchResultList kept rendering the old results
alongside the loading spinner until the new response landed -- every
search appeared to be one query behind. Reset hits on requery, matching
ElasticSearchQuerier's existing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… API

Adds /api/natural-language-search (and its same-origin wrapper), which
elaborates a user query with an LLM (model configurable via
NATURAL_LANGUAGE_SEARCH_MODEL, default claude-sonnet-5) into a verbose
English query and a Hebrew translation, then runs both through semantic
search in parallel threads -- 40 results per leg, no per-leg linked-ref
lookup. The two chunk sets are unioned by ref, and linked refs are
computed once over the unioned set. SemanticSearchQuerier.jsx now points
at the new endpoint.

Also refactors semantic_search/linked_refs.py so the first hop reads the
linked_refs already stored on each chunk instead of querying Mongo's
links collection -- zero extra DB calls for the depth used in
production, and a pgvector-backed lookup (instead of Mongo LinkSet) for
any deeper hops.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two bugs were causing every /api/natural-language-search request to fail
with a 502: ChatAnthropic was called with temperature=0, which
claude-sonnet-5 rejects outright ("temperature is deprecated for this
model"), and _get_llm() only checked Django settings for
ANTHROPIC_API_KEY, ignoring the environment variable convention used
elsewhere in the codebase (sefaria/helper/linker/disambiguator.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… relevance scoring and summaries

Converts the natural-language-search pipeline (query expansion -> parallel
EN/HE semantic search -> link expansion) into a polled Celery task with
per-phase progress. Adds an LLM relevance-scoring pass (1-5, keep 3-5,
stably sorted) and a per-result LLM relevance summary, merges linked-ref
matches into the main results list annotated with their source
(english/hebrew/both/linked), and surfaces the expanded EN/HE queries to
the client as soon as they're available.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The task pod (helm-chart/sefaria/templates/rollout/task.yaml) already had
pgvector and Anthropic secrets but never got the Gemini secret, so any
in-task call to the embedding API (query embedding for semantic search)
failed with "GEMINI_API_KEY is not configured" even though the web pod
had it. Mirrors web.yaml's existing envFrom entry via the shared
sefaria.secrets.geminiApiKey helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant