Vasanth6 commited on
Commit
bd53034
·
1 Parent(s): 137b48f

RAG retreival updates

Browse files
README.md CHANGED
@@ -22,38 +22,44 @@ Visualize and compare **5 chunking strategies** side-by-side:
22
  | **Parent-Child** | Two-level nested chunking — large parent windows with smaller child chunks inside |
23
  | **Semantic** | Detects topic shifts using embedding similarity + adaptive thresholding |
24
 
25
- - **Document X-Ray Viewer** — Original text with color-coded chunk boundaries and overlap regions
26
- - **Chunk Inspector** — Stats panel showing total chunks, average token count, and per-chunk metadata
 
27
 
28
  ### 🌌 Phase 2 — Embedding Lab
29
 
30
- - Generate embeddings using **3 local Ollama embedding models** (Nomic Embed Text, Embedding Gemma, Qwen3 Embedding)
31
- - **UMAP dimensionality reduction** projects high-dimensional embeddings down to 2D
32
- - **Interactive Canvas** with pan, zoom, hover tooltips, and click-to-select
33
- - Parent-child connection lines visualized in vector space
34
 
35
- ### 🔍 Phase 3 — Retrieval
36
 
37
- - **ChromaDB** persistent vector store — chunks are indexed on every run
38
- - **Sonar Query Simulator** — type a natural language query and watch the retrieval happen in real time
39
- - Retrieved chunks render as ranked result cards with distance scores
40
- - **Sonar Probe** — click anywhere on the canvas to find the nearest chunks by 2D proximity
41
- - **Document X-Ray Highlighting** — retrieved chunks glow in the original text with rank-based styling (gold for Rank 1, dashed for Rank 2, dotted for Rank 3)
 
 
 
 
 
42
 
43
  ### ⚔️ Phase 3.2 — LLM-as-a-Judge (The Grand Arena)
44
 
45
- - **Side-by-Side Comparison** — Compare retrieval results from two different models/strategies in a split-screen arena
46
- - **AI Referee** — Call upon a local Ollama model to evaluate, rank, and score retrieved contexts
47
- - **Multi-Dimensional Scorecard** — Referee grades chunks on Relevance, Completeness, Factual Plausibility, and Clarity
48
- - **Pydantic Validator Guardrails** — Validates the referee's output to catch and override arithmetic lies and position bias
49
 
50
  ![Grand Arena Comparison](assets/arena_comparison.gif)
51
 
52
  ### 📐 Phase 4 — Adaptive Thresholding (Gradient Fix)
53
 
54
- - Semantic chunking uses an **adaptive gradient derivative / peak detection** algorithm instead of a static threshold split
55
- - Computes the dynamic threshold based on document-wide mean and standard deviation of inter-sentence embedding distances
56
- - Uses local maxima peak detection to prevent fragmenting paragraphs, ensuring splits only happen at true topic shift peaks
57
 
58
  ---
59
 
@@ -260,7 +266,7 @@ Chunks input text, generates embeddings, reduces to 2D, and stores in ChromaDB.
260
 
261
  ### `POST /api/retrieve`
262
 
263
- Embeds a query and retrieves the top-K most similar chunks from ChromaDB.
264
 
265
  **Request Body:**
266
 
@@ -269,11 +275,15 @@ Embeds a query and retrieves the top-K most similar chunks from ChromaDB.
269
  "search_text": "What is gradient descent?",
270
  "embedding_model": "nomic-embed-text",
271
  "strategy": "fixed_size",
272
- "top_k": 3
 
 
 
 
273
  }
274
  ```
275
 
276
- **Response:** `QueryResponse` with query coordinates, retrieved chunks, and distance scores.
277
 
278
  ### `POST /api/compare`
279
 
@@ -288,7 +298,11 @@ Compares retrieval results from two different configurations side-by-side.
288
  "model_a": "nomic-embed-text",
289
  "strategy_a": "fixed_size",
290
  "model_b": "EmbeddingGemma",
291
- "strategy_b": "semantic"
 
 
 
 
292
  }
293
  ```
294
 
 
22
  | **Parent-Child** | Two-level nested chunking — large parent windows with smaller child chunks inside |
23
  | **Semantic** | Detects topic shifts using embedding similarity + adaptive thresholding |
24
 
25
+ - **Document X-Ray Viewer** — Original text with color-coded chunk boundaries and overlap regions.
26
+ - **Chunk Inspector** — Stats panel showing total chunks, average token count, and per-chunk metadata.
27
+ - **File Uploader** — Attach and parse custom text or markdown documents directly in the configuration panel.
28
 
29
  ### 🌌 Phase 2 — Embedding Lab
30
 
31
+ - Generate embeddings using **3 local Ollama embedding models** (Nomic Embed Text, Embedding Gemma, Qwen3 Embedding).
32
+ - **UMAP dimensionality reduction** projects high-dimensional embeddings down to 2D.
33
+ - **Interactive Canvas** with pan, zoom, hover tooltips, and click-to-select. Drag-panning is isolated from clicks to ensure smooth navigation without losing focus.
34
+ - Parent-child connection lines visualized in vector space.
35
 
36
+ ### 🔍 Phase 3 — Advanced Retrieval & Reranking
37
 
38
+ - **ChromaDB** persistent vector store — chunks are indexed on every run.
39
+ - **Flexible Retrieval Modes** — Switch dynamically between **Dense** (vector similarity), **Sparse** (BM25 lexical search), or **Hybrid** (RRF fusion) search paths.
40
+ - **Sonar Query Simulator** Type a natural language query and watch the sonar ping animate across the canvas in real time.
41
+ - **Sonar Probe** — Click anywhere on the 2D canvas to retrieve the nearest chunks in that region.
42
+ - **Document X-Ray Highlighting** — Retrieved chunks glow dynamically in the document viewer with rank-based styling (gold for Rank 1, dashed for Rank 2, dotted for Rank 3).
43
+ - **Metadata Level Filtering** — Filter your context pool on the fly (retrieve *Only Parents*, *Only Children*, or *All Levels*).
44
+ - **Cross-Encoder Reranking** — Run a local FlashRank (`ms-marco-MiniLM-L-12-v2`) engine to rerank search results.
45
+ - **Rank Shift Badges** — Visual indicators showing exactly how much chunks moved after reranking (`▲ +3`, `▼ -1`, or `• Unchanged`).
46
+ - **Normalized Match Strength** — Converts raw vector distances into intuitive similarity percentages (e.g. `Match: 87.7%`).
47
+ - **Reranking Lineage** — Displays the pre-reranked retrieval score for comparison (e.g. `Match: 95.0% (was Match: 87.7%)`).
48
 
49
  ### ⚔️ Phase 3.2 — LLM-as-a-Judge (The Grand Arena)
50
 
51
+ - **Side-by-Side Comparison** — Compare retrieval results from two different models/strategies in a split-screen arena.
52
+ - **AI Referee** — Call upon a local Ollama model to evaluate, rank, and score retrieved contexts.
53
+ - **Multi-Dimensional Scorecard** — Referee grades chunks on Relevance, Completeness, Factual Plausibility, and Clarity.
54
+ - **Pydantic Validator Guardrails** — Validates the referee's output to catch and override arithmetic lies and position bias.
55
 
56
  ![Grand Arena Comparison](assets/arena_comparison.gif)
57
 
58
  ### 📐 Phase 4 — Adaptive Thresholding (Gradient Fix)
59
 
60
+ - Semantic chunking uses an **adaptive gradient derivative / peak detection** algorithm instead of a static threshold split.
61
+ - Computes the dynamic threshold based on document-wide mean and standard deviation of inter-sentence embedding distances.
62
+ - Uses local maxima peak detection to prevent fragmenting paragraphs, ensuring splits only happen at true topic shift peaks.
63
 
64
  ---
65
 
 
266
 
267
  ### `POST /api/retrieve`
268
 
269
+ Embeds a query and retrieves the top-K most similar chunks from ChromaDB (with optional reranking, HyDE expansion, and metadata filtering).
270
 
271
  **Request Body:**
272
 
 
275
  "search_text": "What is gradient descent?",
276
  "embedding_model": "nomic-embed-text",
277
  "strategy": "fixed_size",
278
+ "top_k": 3,
279
+ "retrieval_mode": "dense",
280
+ "use_hyde": false,
281
+ "use_reranking": true,
282
+ "metadata": { "level": 1 }
283
  }
284
  ```
285
 
286
+ **Response:** `QueryResponse` with query coordinates, retrieved chunks (with original ranks and original scores populated if reranked), and hypothetical answer text if HyDE is used.
287
 
288
  ### `POST /api/compare`
289
 
 
298
  "model_a": "nomic-embed-text",
299
  "strategy_a": "fixed_size",
300
  "model_b": "EmbeddingGemma",
301
+ "strategy_b": "semantic",
302
+ "retrieval_mode": "dense",
303
+ "use_hyde": false,
304
+ "use_reranking": true,
305
+ "metadata": null
306
  }
307
  ```
308
 
backend/engines/{BM25Enginer.py → bm25_engine.py} RENAMED
@@ -1,7 +1,7 @@
1
  import math
2
- from typing import Any, Dict, List
3
 
4
- from backend.routers.retrieval_router import get_keywords
5
 
6
 
7
  class BM25Engine:
@@ -58,10 +58,21 @@ class BM25Engine:
58
 
59
  return total_score
60
 
61
- def search(self, query: str, top_k: int = 3) -> list:
 
 
62
  scores = []
63
  # Score all docs
64
  for idx in range(self.corpus_size):
 
 
 
 
 
 
 
 
 
65
  s = self.score(query, idx)
66
  scores.append((s, idx))
67
 
 
1
  import math
2
+ from typing import Any, Dict, List, Optional
3
 
4
+ from backend.utils import get_keywords
5
 
6
 
7
  class BM25Engine:
 
58
 
59
  return total_score
60
 
61
+ def search(
62
+ self, query: str, top_k: int = 3, where: Optional[Dict[str, Any]] = None
63
+ ) -> list:
64
  scores = []
65
  # Score all docs
66
  for idx in range(self.corpus_size):
67
+ if where:
68
+ match = True
69
+ doc_meta = self.metadatas[idx] or {}
70
+ for key, val in where.items():
71
+ if doc_meta.get(key) != val:
72
+ match = False
73
+ break
74
+ if not match:
75
+ continue
76
  s = self.score(query, idx)
77
  scores.append((s, idx))
78
 
backend/engines/re_ranker_engine.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from flashrank import Ranker, RerankRequest
3
+ from backend.models.schemas import RetrievedChunk
4
+
5
+
6
+ class RerankerEngine:
7
+ _ranker = None
8
+
9
+ def __init__(self):
10
+ if RerankerEngine._ranker is None:
11
+ RerankerEngine._ranker = Ranker(model_name="ms-marco-MiniLM-L-12-v2")
12
+ self.ranker = RerankerEngine._ranker
13
+
14
+ def rerank(self, query: str, chunks: List[RetrievedChunk], top_k: int):
15
+ formatted_list = []
16
+ for idx, chunk in enumerate(chunks):
17
+ chunk.original_score = chunk.score
18
+ chunk.original_rank = idx + 1
19
+ formatted_list.append(
20
+ {
21
+ "id": chunk.id,
22
+ "text": chunk.text,
23
+ "score": chunk.score,
24
+ }
25
+ )
26
+
27
+ request = RerankRequest(query=query, passages=formatted_list)
28
+ results = self.ranker.rerank(request)
29
+ chunk_map = {chunk.id: chunk for chunk in chunks}
30
+ reranked_chunks = []
31
+
32
+ for result in results[:top_k]:
33
+ chunk = chunk_map[result["id"]]
34
+ chunk.score = float(result["score"])
35
+ reranked_chunks.append(chunk)
36
+
37
+ return reranked_chunks
backend/models/schemas.py CHANGED
@@ -86,6 +86,7 @@ class QueryRequest(BaseModel):
86
  retrieval_mode: RetrievalMode = RetrievalMode.DENSE
87
  use_hyde: bool = False
88
  use_reranking: bool = False
 
89
 
90
 
91
  class RetrievedChunk(BaseModel):
@@ -97,6 +98,8 @@ class RetrievedChunk(BaseModel):
97
  parent_id: Optional[str] = None
98
  level: int = 0
99
  text_highlighted: Optional[str] = None
 
 
100
 
101
 
102
  class QueryResponse(BaseModel):
@@ -117,6 +120,7 @@ class CompareRequest(BaseModel):
117
  retrieval_mode: RetrievalMode = RetrievalMode.DENSE
118
  use_hyde: bool = False
119
  use_reranking: bool = False
 
120
 
121
 
122
  class CompareResponse(BaseModel):
 
86
  retrieval_mode: RetrievalMode = RetrievalMode.DENSE
87
  use_hyde: bool = False
88
  use_reranking: bool = False
89
+ metadata: Optional[Dict[str, Any]] = None
90
 
91
 
92
  class RetrievedChunk(BaseModel):
 
98
  parent_id: Optional[str] = None
99
  level: int = 0
100
  text_highlighted: Optional[str] = None
101
+ original_score: Optional[float] = None
102
+ original_rank: Optional[int] = None
103
 
104
 
105
  class QueryResponse(BaseModel):
 
120
  retrieval_mode: RetrievalMode = RetrievalMode.DENSE
121
  use_hyde: bool = False
122
  use_reranking: bool = False
123
+ metadata: Optional[Dict[str, Any]] = None
124
 
125
 
126
  class CompareResponse(BaseModel):
backend/routers/retrieval_router.py CHANGED
@@ -1,12 +1,11 @@
1
- from backend.constants import hyde_prompt
2
- from backend.engines import llm_client
3
  from backend.engines.llm_client import OllamaClient
4
  from backend.constants import system_instructions
5
  import asyncio
6
- from typing import Any, List, Optional
7
  from fastapi import APIRouter
8
  import json
9
- import re
10
  from backend.engines.embedding import EmbeddingEngine
11
  from backend.engines.reducer import ReducerEngine
12
  from backend.models.schemas import (
@@ -20,17 +19,11 @@ from backend.models.schemas import (
20
  RetrievedChunk,
21
  )
22
  from backend.storage.vector_store import VectorStore
23
-
24
- from nltk.tokenize import RegexpTokenizer
25
- from nltk.corpus import stopwords
26
 
27
  router = APIRouter(prefix="/api", tags=["retrieval"])
28
 
29
 
30
- tokenizer = RegexpTokenizer(r"\w+")
31
- stop_words = set(stopwords.words("english"))
32
-
33
-
34
  @router.post("/retrieve", response_model=QueryResponse)
35
  async def retrieve(request: QueryRequest):
36
 
@@ -40,18 +33,24 @@ async def retrieve(request: QueryRequest):
40
  else request.search_text
41
  )
42
 
43
- if request.use_reranking == True:
44
- "" ""
45
 
46
  retrieved_chunks, embeddings = await process_retrieval(
47
  request.embedding_model,
48
  search_text,
49
  request.strategy,
50
- request.top_k,
51
- request.search_text,
52
  request.retrieval_mode,
 
 
53
  )
54
 
 
 
 
 
 
 
55
  query_coords = [0.0, 0.0]
56
  if ReducerEngine._last_fitted_reducer is not None:
57
  projected: Any = ReducerEngine._last_fitted_reducer.transform(embeddings)
@@ -76,6 +75,7 @@ async def compare(request: CompareRequest):
76
  if request.use_hyde
77
  else request.search_text
78
  )
 
79
  (
80
  (retrieved_chunks_a, _),
81
  (retrieved_chunks_b, _),
@@ -84,18 +84,35 @@ async def compare(request: CompareRequest):
84
  request.model_a,
85
  retrieval_text,
86
  request.strategy_a,
87
- request.top_k,
 
88
  request.search_text,
 
89
  ),
90
  process_retrieval(
91
  request.model_b,
92
  retrieval_text,
93
  request.strategy_b,
94
- request.top_k,
 
95
  request.search_text,
 
96
  ),
97
  )
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  response_kwargs = {
100
  "search_text": request.search_text,
101
  "results_a": retrieved_chunks_a,
@@ -130,25 +147,87 @@ async def process_retrieval(
130
  top_k,
131
  retrieval_mode,
132
  original_query: Optional[str] = None,
 
133
  ):
134
  vector_store = VectorStore()
135
  embedding_engine = EmbeddingEngine(model.value)
136
  embeddings = await embedding_engine.generate_embeddings([search_text])
137
  collection_name = f"{model.value}_{strategy.value}".replace(":", "-")
138
- retrieval_response: Any
139
  if retrieval_mode == RetrievalMode.DENSE:
140
- """"""
 
 
 
 
 
141
  elif retrieval_mode == RetrievalMode.SPARSE:
142
- """"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  elif retrieval_mode == RetrievalMode.HYBRID:
144
- """"""
145
-
146
- result: Any = await vector_store.retrieve(
147
- collection_name=collection_name,
148
- embeddings=embeddings,
149
- n_results=top_k,
150
- )
151
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  retrieved_chunks = []
153
 
154
  highlight_text = original_query if original_query is not None else search_text
@@ -177,41 +256,3 @@ async def process_retrieval(
177
  )
178
 
179
  return retrieved_chunks, embeddings
180
-
181
-
182
- def get_text_highlights(original_text, search_text):
183
- query_keywords = get_keywords(search_text)
184
- if not query_keywords:
185
- return original_text
186
-
187
- query_keywords.sort(key=len, reverse=True)
188
-
189
- safe_keywords = [f"{re.escape(w)}(?:'s)?" for w in query_keywords]
190
-
191
- pattern_string = r"\b(" + "|".join(safe_keywords) + r")\b"
192
- pattern = re.compile(pattern_string, re.IGNORECASE)
193
-
194
- return pattern.sub(r"<mark>\1</mark>", original_text)
195
-
196
-
197
- def get_keywords(text: str):
198
- token = tokenizer.tokenize(text.lower())
199
- return [word for word in token if word not in stop_words]
200
-
201
-
202
- async def get_hyde_text(search_text):
203
- llm_client = OllamaClient()
204
- hyde_prompt.format(search_text=search_text)
205
- return await llm_client.generate(hyde_prompt, response_format=None)
206
-
207
-
208
- def rrf(dense_ranks: List[str], sparse_ranks: List[str], k: int = 60) -> List[tuple]:
209
- rrf_scores = {}
210
-
211
- for rank, doc_id in enumerate(dense_ranks):
212
- rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + 1.0 / (k + rank + 1)
213
-
214
- for rank, doc_id in enumerate(sparse_ranks):
215
- rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + 1.0 / (k + rank + 1)
216
-
217
- return sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
 
1
+ from backend.engines.re_ranker_engine import RerankerEngine
2
+ from backend.engines.bm25_engine import BM25Engine
3
  from backend.engines.llm_client import OllamaClient
4
  from backend.constants import system_instructions
5
  import asyncio
6
+ from typing import Any, Dict, List, Optional
7
  from fastapi import APIRouter
8
  import json
 
9
  from backend.engines.embedding import EmbeddingEngine
10
  from backend.engines.reducer import ReducerEngine
11
  from backend.models.schemas import (
 
19
  RetrievedChunk,
20
  )
21
  from backend.storage.vector_store import VectorStore
22
+ from backend.utils import get_hyde_text, get_keywords, get_text_highlights, rrf
 
 
23
 
24
  router = APIRouter(prefix="/api", tags=["retrieval"])
25
 
26
 
 
 
 
 
27
  @router.post("/retrieve", response_model=QueryResponse)
28
  async def retrieve(request: QueryRequest):
29
 
 
33
  else request.search_text
34
  )
35
 
36
+ limit = request.top_k * 4 if request.use_reranking else request.top_k
 
37
 
38
  retrieved_chunks, embeddings = await process_retrieval(
39
  request.embedding_model,
40
  search_text,
41
  request.strategy,
42
+ limit,
 
43
  request.retrieval_mode,
44
+ request.search_text,
45
+ request.metadata,
46
  )
47
 
48
+ if request.use_reranking == True:
49
+ re_ranker = RerankerEngine()
50
+ retrieved_chunks = re_ranker.rerank(
51
+ query=request.search_text, chunks=retrieved_chunks, top_k=request.top_k
52
+ )
53
+
54
  query_coords = [0.0, 0.0]
55
  if ReducerEngine._last_fitted_reducer is not None:
56
  projected: Any = ReducerEngine._last_fitted_reducer.transform(embeddings)
 
75
  if request.use_hyde
76
  else request.search_text
77
  )
78
+ limit = request.top_k * 4 if request.use_reranking else request.top_k
79
  (
80
  (retrieved_chunks_a, _),
81
  (retrieved_chunks_b, _),
 
84
  request.model_a,
85
  retrieval_text,
86
  request.strategy_a,
87
+ limit,
88
+ request.retrieval_mode,
89
  request.search_text,
90
+ request.metadata,
91
  ),
92
  process_retrieval(
93
  request.model_b,
94
  retrieval_text,
95
  request.strategy_b,
96
+ limit,
97
+ request.retrieval_mode,
98
  request.search_text,
99
+ request.metadata,
100
  ),
101
  )
102
 
103
+ if request.use_reranking == True:
104
+ re_ranker = RerankerEngine()
105
+ retrieved_chunks_a = re_ranker.rerank(
106
+ query=request.search_text,
107
+ chunks=retrieved_chunks_a,
108
+ top_k=request.top_k,
109
+ )
110
+ retrieved_chunks_b = re_ranker.rerank(
111
+ query=request.search_text,
112
+ chunks=retrieved_chunks_b,
113
+ top_k=request.top_k,
114
+ )
115
+
116
  response_kwargs = {
117
  "search_text": request.search_text,
118
  "results_a": retrieved_chunks_a,
 
147
  top_k,
148
  retrieval_mode,
149
  original_query: Optional[str] = None,
150
+ metadata: Optional[Dict[str, Any]] = None,
151
  ):
152
  vector_store = VectorStore()
153
  embedding_engine = EmbeddingEngine(model.value)
154
  embeddings = await embedding_engine.generate_embeddings([search_text])
155
  collection_name = f"{model.value}_{strategy.value}".replace(":", "-")
156
+ result: Any = None
157
  if retrieval_mode == RetrievalMode.DENSE:
158
+ result: Any = await vector_store.retrieve(
159
+ collection_name=collection_name,
160
+ embeddings=embeddings,
161
+ n_results=top_k,
162
+ where=metadata,
163
+ )
164
  elif retrieval_mode == RetrievalMode.SPARSE:
165
+ all_data = await vector_store.get_all_documents(collection_name=collection_name)
166
+ docs = all_data.get("documents") or []
167
+ doc_ids = all_data.get("ids") or []
168
+ meta_datas: List[Any] = all_data.get("metadatas", []) or []
169
+
170
+ if not docs:
171
+ result = {
172
+ "ids": [[]],
173
+ "documents": [[]],
174
+ "distances": [[]],
175
+ "metadatas": [[]],
176
+ }
177
+ else:
178
+ bm25 = BM25Engine(documents=docs, doc_ids=doc_ids, metadatas=meta_datas)
179
+ sparse_results = bm25.search(query=search_text, top_k=top_k, where=metadata)
180
+ result = {
181
+ "ids": [[res["id"] for res in sparse_results]],
182
+ "documents": [[res["text"] for res in sparse_results]],
183
+ "distances": [[res["score"] for res in sparse_results]],
184
+ "metadatas": [[res["metadata"] for res in sparse_results]],
185
+ }
186
  elif retrieval_mode == RetrievalMode.HYBRID:
187
+ dense_res = await vector_store.retrieve(
188
+ collection_name=collection_name,
189
+ embeddings=embeddings,
190
+ n_results=top_k * 2,
191
+ where=metadata,
192
+ )
193
+ dense_ids = (
194
+ dense_res.get("ids", [[]])[0] if dense_res and "ids" in dense_res else []
195
+ )
196
+ all_data = await vector_store.get_all_documents(collection_name)
197
+ docs = all_data.get("documents", []) or []
198
+ ids = all_data.get("ids", []) or []
199
+ metadatas: List[Any] = all_data.get("metadatas", []) or []
200
+
201
+ if not docs:
202
+ result = {
203
+ "ids": [[]],
204
+ "documents": [[]],
205
+ "distances": [[]],
206
+ "metadatas": [[]],
207
+ }
208
+ else:
209
+ bm25 = BM25Engine(documents=docs, doc_ids=ids, metadatas=metadatas)
210
+ sparse_results = bm25.search(search_text, top_k=top_k * 2, where=metadata)
211
+ sparse_ids = [res["id"] for res in sparse_results]
212
+
213
+ # 2. Build a lookup map of the document contents
214
+ doc_lookup = {
215
+ ids[i]: {"text": docs[i], "metadata": metadatas[i]}
216
+ for i in range(len(ids))
217
+ }
218
+
219
+ # 3. Fuse rankings
220
+ fused_ranks = rrf(dense_ids, sparse_ids, k=60)[:top_k]
221
+
222
+ # 4. Standardize output
223
+ result = {
224
+ "ids": [[item[0] for item in fused_ranks]],
225
+ "documents": [[doc_lookup[item[0]]["text"] for item in fused_ranks]],
226
+ "distances": [[item[1] for item in fused_ranks]], # RRF score
227
+ "metadatas": [
228
+ [doc_lookup[item[0]]["metadata"] for item in fused_ranks]
229
+ ],
230
+ }
231
  retrieved_chunks = []
232
 
233
  highlight_text = original_query if original_query is not None else search_text
 
256
  )
257
 
258
  return retrieved_chunks, embeddings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/storage/vector_store.py CHANGED
@@ -25,11 +25,12 @@ class VectorStore:
25
  metadatas=metadatas,
26
  )
27
 
28
- async def retrieve(self, collection_name, embeddings, n_results=3):
29
  collection = self.get_collection(collection_name)
30
 
31
  return await asyncio.to_thread(
32
  collection.query,
 
33
  query_embeddings=embeddings,
34
  n_results=n_results,
35
  )
 
25
  metadatas=metadatas,
26
  )
27
 
28
+ async def retrieve(self, collection_name, embeddings, where, n_results=3):
29
  collection = self.get_collection(collection_name)
30
 
31
  return await asyncio.to_thread(
32
  collection.query,
33
+ where=where,
34
  query_embeddings=embeddings,
35
  n_results=n_results,
36
  )
backend/utils.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import List
3
+ from backend.constants import hyde_prompt
4
+ from backend.engines.llm_client import OllamaClient
5
+ from nltk.tokenize import RegexpTokenizer
6
+ from nltk.corpus import stopwords
7
+
8
+ tokenizer = RegexpTokenizer(r"\w+")
9
+ stop_words = set(stopwords.words("english"))
10
+
11
+
12
+ def get_keywords(text: str):
13
+ token = tokenizer.tokenize(text.lower())
14
+ return [word for word in token if word not in stop_words]
15
+
16
+
17
+ async def get_hyde_text(search_text):
18
+ llm_client = OllamaClient()
19
+ formatted_prompt = hyde_prompt.format(search_text=search_text)
20
+ return await llm_client.generate(formatted_prompt, response_format=None)
21
+
22
+
23
+ def rrf(dense_ranks: List[str], sparse_ranks: List[str], k: int = 60) -> List[tuple]:
24
+ rrf_scores = {}
25
+
26
+ for rank, doc_id in enumerate(dense_ranks):
27
+ rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + 1.0 / (k + rank + 1)
28
+
29
+ for rank, doc_id in enumerate(sparse_ranks):
30
+ rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + 1.0 / (k + rank + 1)
31
+
32
+ return sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
33
+
34
+
35
+ def get_text_highlights(original_text, search_text):
36
+ query_keywords = get_keywords(search_text)
37
+ if not query_keywords:
38
+ return original_text
39
+
40
+ query_keywords.sort(key=len, reverse=True)
41
+
42
+ safe_keywords = [f"{re.escape(w)}(?:'s)?" for w in query_keywords]
43
+
44
+ pattern_string = r"\b(" + "|".join(safe_keywords) + r")\b"
45
+ pattern = re.compile(pattern_string, re.IGNORECASE)
46
+
47
+ return pattern.sub(r"<mark>\1</mark>", original_text)
frontend/app.js CHANGED
@@ -173,6 +173,29 @@ dom.textInput.addEventListener("input", () => {
173
  dom.charCount.textContent = `${state.text.length} chars`;
174
  });
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  // ============================================================
177
  // Strategy Selector
178
  // ============================================================
@@ -492,7 +515,8 @@ function renderChunkList(chunks) {
492
  function highlightChunk(chunkId) {
493
  // Highlight in X-ray viewer
494
  dom.xrayText.querySelectorAll(".chunk-highlight").forEach((el) => {
495
- el.classList.toggle("active", el.dataset.chunkId === chunkId);
 
496
  });
497
 
498
  // Highlight in chunk list
@@ -544,6 +568,49 @@ function escapeHtml(str) {
544
  return div.innerHTML;
545
  }
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  document.addEventListener("keydown", (e) => {
548
  // Ctrl/Cmd + Enter to run
549
  if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
@@ -940,6 +1007,7 @@ function setupCanvasListeners(canvas) {
940
  canvas.addEventListener("mousedown", (e) => {
941
  const pos = getMousePos(e);
942
  vectorState.isDragging = true;
 
943
  vectorState.dragStart = {
944
  x: pos.x - vectorState.pan.x,
945
  y: pos.y - vectorState.pan.y,
@@ -1029,8 +1097,16 @@ function setupCanvasListeners(canvas) {
1029
  );
1030
 
1031
  canvas.addEventListener("click", (e) => {
1032
- if (vectorState.isDragging) return;
1033
  const pos = getMousePos(e);
 
 
 
 
 
 
 
 
 
1034
  const width = canvas.width / window.devicePixelRatio;
1035
  const height = canvas.height / window.devicePixelRatio;
1036
 
@@ -1193,6 +1269,16 @@ async function runQuerySimulator() {
1193
  <div class="skeleton-loader"></div>
1194
  `;
1195
 
 
 
 
 
 
 
 
 
 
 
1196
  // Make the actual API call to the backend
1197
  const res = await fetch("/api/retrieve", {
1198
  method: "POST", // Needs to be POST to send a JSON body!
@@ -1203,6 +1289,9 @@ async function runQuerySimulator() {
1203
  strategy: state.strategy,
1204
  top_k: 3,
1205
  retrieval_mode: dom.queryRetrievalMode ? dom.queryRetrievalMode.value : "dense",
 
 
 
1206
  }),
1207
  });
1208
 
@@ -1242,6 +1331,8 @@ async function runQuerySimulator() {
1242
  chunk: chunk,
1243
  dist: result.score,
1244
  text_highlighted: result.text_highlighted,
 
 
1245
  };
1246
  });
1247
 
@@ -1259,20 +1350,43 @@ async function runQuerySimulator() {
1259
  };
1260
 
1261
  // Render results in the drawer
1262
- dom.queryResultsList.innerHTML = topK
 
 
 
 
 
 
 
 
 
 
 
 
1263
  .map((item, index) => {
 
1264
  return `
1265
  <div class="query-result-item" style="border-left-color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">
1266
  <div class="query-result-header">
1267
- <span class="rank-badge" style="color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">Rank ${index + 1} (${item.chunk.id})</span>
1268
- <span class="dist-badge">Dist: ${item.dist.toFixed(3)}</span>
 
 
 
 
 
1269
  </div>
1270
  <div class="query-result-text">${item.text_highlighted || escapeHtml(item.chunk.text.substring(0, 100)) + "..."}</div>
 
 
 
1271
  </div>
1272
  `;
1273
  })
1274
  .join("");
1275
 
 
 
1276
  // --- TASK 3.4: X-RAY DOCUMENT HIGHLIGHTING ---
1277
  // 1. Activate the search mode on the X-Ray text to dim non-retrieved chunks
1278
  dom.xrayText.classList.add("search-active");
@@ -1290,14 +1404,15 @@ async function runQuerySimulator() {
1290
  topK.forEach((item, index) => {
1291
  const rank = index + 1;
1292
  const chunkId = item.chunk.id;
1293
- const span = dom.xrayText.querySelector(`[data-chunk-id="${chunkId}"]`);
1294
 
1295
- if (span) {
1296
  span.classList.add(`retrieved-rank-${rank}`);
1297
- // If it's the #1 hit, scroll the Document Viewer straight to it!
1298
- if (rank === 1) {
1299
- span.scrollIntoView({ behavior: "smooth", block: "center" });
1300
- }
 
1301
  }
1302
  });
1303
  // ----------------------------------------------
@@ -1376,12 +1491,27 @@ if (domArena.btnFight) {
1376
  domReferee.triggerBox.style.display = "block";
1377
  }
1378
 
 
 
 
 
 
1379
  domArena.resultsA.innerHTML =
1380
  '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1381
  domArena.resultsB.innerHTML =
1382
  '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1383
 
1384
  try {
 
 
 
 
 
 
 
 
 
 
1385
  const res = await fetch("/api/compare", {
1386
  method: "POST",
1387
  headers: { "Content-Type": "application/json" },
@@ -1393,6 +1523,9 @@ if (domArena.btnFight) {
1393
  model_b: domArena.modelB.value,
1394
  strategy_b: domArena.strategyB.value,
1395
  retrieval_mode: domArena.retrievalMode ? domArena.retrievalMode.value : "dense",
 
 
 
1396
  }),
1397
  });
1398
 
@@ -1403,16 +1536,35 @@ if (domArena.btnFight) {
1403
 
1404
  const data = await res.json();
1405
 
 
 
 
 
 
 
 
 
 
 
 
1406
  // Render Column A
1407
  domArena.resultsA.innerHTML = data.results_a
1408
  .map(
1409
  (chunk, i) => `
1410
  <div class="query-result-item" style="border-left-color: var(--warning-color)">
1411
  <div class="query-result-header">
1412
- <span class="rank-badge" style="color: var(--warning-color)">Rank ${i + 1}</span>
1413
- <span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
 
 
 
 
 
1414
  </div>
1415
  <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
 
 
 
1416
  </div>
1417
  `,
1418
  )
@@ -1424,10 +1576,18 @@ if (domArena.btnFight) {
1424
  (chunk, i) => `
1425
  <div class="query-result-item" style="border-left-color: var(--info-color)">
1426
  <div class="query-result-header">
1427
- <span class="rank-badge" style="color: var(--info-color)">Rank ${i + 1}</span>
1428
- <span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
 
 
 
 
 
1429
  </div>
1430
  <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
 
 
 
1431
  </div>
1432
  `,
1433
  )
 
173
  dom.charCount.textContent = `${state.text.length} chars`;
174
  });
175
 
176
+ const btnUploadFile = document.getElementById("btn-upload-file");
177
+ const fileInput = document.getElementById("file-input");
178
+
179
+ if (btnUploadFile && fileInput) {
180
+ btnUploadFile.addEventListener("click", () => fileInput.click());
181
+
182
+ fileInput.addEventListener("change", (e) => {
183
+ const file = e.target.files[0];
184
+ if (!file) return;
185
+
186
+ const reader = new FileReader();
187
+ reader.onload = function (event) {
188
+ const text = event.target.result;
189
+ dom.textInput.value = text;
190
+ state.text = text;
191
+ dom.charCount.textContent = `${text.length} chars`;
192
+ fileInput.value = "";
193
+ };
194
+ reader.readAsText(file);
195
+ });
196
+ }
197
+
198
+
199
  // ============================================================
200
  // Strategy Selector
201
  // ============================================================
 
515
  function highlightChunk(chunkId) {
516
  // Highlight in X-ray viewer
517
  dom.xrayText.querySelectorAll(".chunk-highlight").forEach((el) => {
518
+ const ownerList = (el.dataset.allChunks || "").split(",");
519
+ el.classList.toggle("active", ownerList.includes(chunkId));
520
  });
521
 
522
  // Highlight in chunk list
 
568
  return div.innerHTML;
569
  }
570
 
571
+ function getRankShiftBadgeHtml(originalRank, currentRank) {
572
+ if (originalRank === undefined || originalRank === null) {
573
+ return "";
574
+ }
575
+ const shift = originalRank - currentRank;
576
+ if (shift > 0) {
577
+ return `<span class="rank-shift-badge rank-shift-up">▲ +${shift}</span>`;
578
+ } else if (shift < 0) {
579
+ return `<span class="rank-shift-badge rank-shift-down">▼ ${shift}</span>`;
580
+ } else {
581
+ return `<span class="rank-shift-badge rank-shift-unchanged">• Unchanged</span>`;
582
+ }
583
+ }
584
+
585
+ function formatScoreHtml(score, originalScore, retrievalMode, useReranking) {
586
+ let label = "Score";
587
+ let formattedScore = score.toFixed(3);
588
+
589
+ if (useReranking) {
590
+ label = "Rerank";
591
+ formattedScore = `${(score * 100).toFixed(1)}%`;
592
+ } else if (retrievalMode === "sparse") {
593
+ label = "BM25";
594
+ formattedScore = score.toFixed(2);
595
+ } else if (retrievalMode === "hybrid") {
596
+ label = "RRF";
597
+ formattedScore = score.toFixed(4);
598
+ } else {
599
+ label = "Dist";
600
+ formattedScore = score.toFixed(3);
601
+ }
602
+
603
+ let html = `<span class="score-badge-val">${label}: ${formattedScore}</span>`;
604
+
605
+ if (useReranking && originalScore !== null && originalScore !== undefined) {
606
+ let origLabel = retrievalMode === "sparse" ? "BM25" : (retrievalMode === "hybrid" ? "RRF" : "Dist");
607
+ let origFormatted = originalScore.toFixed(3);
608
+ html += `<span class="score-badge-original" title="Score before reranking"> (was ${origLabel}: ${origFormatted})</span>`;
609
+ }
610
+
611
+ return html;
612
+ }
613
+
614
  document.addEventListener("keydown", (e) => {
615
  // Ctrl/Cmd + Enter to run
616
  if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
 
1007
  canvas.addEventListener("mousedown", (e) => {
1008
  const pos = getMousePos(e);
1009
  vectorState.isDragging = true;
1010
+ vectorState.dragStartPos = { x: pos.x, y: pos.y };
1011
  vectorState.dragStart = {
1012
  x: pos.x - vectorState.pan.x,
1013
  y: pos.y - vectorState.pan.y,
 
1097
  );
1098
 
1099
  canvas.addEventListener("click", (e) => {
 
1100
  const pos = getMousePos(e);
1101
+ if (vectorState.dragStartPos) {
1102
+ const dx = pos.x - vectorState.dragStartPos.x;
1103
+ const dy = pos.y - vectorState.dragStartPos.y;
1104
+ const dist = Math.hypot(dx, dy);
1105
+ if (dist > 5) {
1106
+ return; // It was a drag, not a click!
1107
+ }
1108
+ }
1109
+ if (vectorState.isDragging) return;
1110
  const width = canvas.width / window.devicePixelRatio;
1111
  const height = canvas.height / window.devicePixelRatio;
1112
 
 
1269
  <div class="skeleton-loader"></div>
1270
  `;
1271
 
1272
+ const useHyde = document.getElementById("query-use-hyde") ? document.getElementById("query-use-hyde").checked : false;
1273
+ const useReranking = document.getElementById("query-use-reranking") ? document.getElementById("query-use-reranking").checked : false;
1274
+ const filterLevel = document.getElementById("query-filter-level") ? document.getElementById("query-filter-level").value : "all";
1275
+ let whereClause = null;
1276
+ if (filterLevel === "parent") {
1277
+ whereClause = { "level": 0 };
1278
+ } else if (filterLevel === "child") {
1279
+ whereClause = { "level": 1 };
1280
+ }
1281
+
1282
  // Make the actual API call to the backend
1283
  const res = await fetch("/api/retrieve", {
1284
  method: "POST", // Needs to be POST to send a JSON body!
 
1289
  strategy: state.strategy,
1290
  top_k: 3,
1291
  retrieval_mode: dom.queryRetrievalMode ? dom.queryRetrievalMode.value : "dense",
1292
+ use_hyde: useHyde,
1293
+ use_reranking: useReranking,
1294
+ metadata: whereClause,
1295
  }),
1296
  });
1297
 
 
1331
  chunk: chunk,
1332
  dist: result.score,
1333
  text_highlighted: result.text_highlighted,
1334
+ original_rank: result.original_rank,
1335
+ original_score: result.original_score,
1336
  };
1337
  });
1338
 
 
1350
  };
1351
 
1352
  // Render results in the drawer
1353
+ let htmlContent = "";
1354
+ if (data.hypothetical_answer) {
1355
+ htmlContent += `
1356
+ <div class="query-result-item" style="border-left-color: var(--superman-blue); background-color: var(--superman-blue-muted); margin-bottom: 12px; border-radius: 6px; padding: 10px;">
1357
+ <div class="query-result-header" style="margin-bottom: 4px;">
1358
+ <span class="rank-badge" style="color: var(--superman-blue); font-weight: bold;">🔮 Generated Hypothetical Answer (HyDE)</span>
1359
+ </div>
1360
+ <div class="query-result-text" style="font-style: italic; color: var(--text-primary); font-size: 0.85rem;">"${escapeHtml(data.hypothetical_answer)}"</div>
1361
+ </div>
1362
+ `;
1363
+ }
1364
+
1365
+ htmlContent += topK
1366
  .map((item, index) => {
1367
+ const retrievalMode = dom.queryRetrievalMode ? dom.queryRetrievalMode.value : "dense";
1368
  return `
1369
  <div class="query-result-item" style="border-left-color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">
1370
  <div class="query-result-header">
1371
+ <span class="rank-badge" style="color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">
1372
+ Rank ${index + 1}
1373
+ ${getRankShiftBadgeHtml(item.original_rank, index + 1)}
1374
+ </span>
1375
+ <span class="dist-badge">
1376
+ ${formatScoreHtml(item.dist, item.original_score, retrievalMode, useReranking)}
1377
+ </span>
1378
  </div>
1379
  <div class="query-result-text">${item.text_highlighted || escapeHtml(item.chunk.text.substring(0, 100)) + "..."}</div>
1380
+ <div class="query-result-meta">
1381
+ <span>ID: ${item.chunk.id}</span>
1382
+ </div>
1383
  </div>
1384
  `;
1385
  })
1386
  .join("");
1387
 
1388
+ dom.queryResultsList.innerHTML = htmlContent;
1389
+
1390
  // --- TASK 3.4: X-RAY DOCUMENT HIGHLIGHTING ---
1391
  // 1. Activate the search mode on the X-Ray text to dim non-retrieved chunks
1392
  dom.xrayText.classList.add("search-active");
 
1404
  topK.forEach((item, index) => {
1405
  const rank = index + 1;
1406
  const chunkId = item.chunk.id;
1407
+ const spans = dom.xrayText.querySelectorAll(`[data-all-chunks*="${chunkId}"]`);
1408
 
1409
+ spans.forEach((span) => {
1410
  span.classList.add(`retrieved-rank-${rank}`);
1411
+ });
1412
+
1413
+ // If it's the #1 hit, scroll the Document Viewer straight to it!
1414
+ if (rank === 1 && spans.length > 0) {
1415
+ spans[0].scrollIntoView({ behavior: "smooth", block: "center" });
1416
  }
1417
  });
1418
  // ----------------------------------------------
 
1491
  domReferee.triggerBox.style.display = "block";
1492
  }
1493
 
1494
+ const hydeContainer = document.getElementById("arena-hyde-container");
1495
+ if (hydeContainer) {
1496
+ hydeContainer.style.display = "none";
1497
+ }
1498
+
1499
  domArena.resultsA.innerHTML =
1500
  '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1501
  domArena.resultsB.innerHTML =
1502
  '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1503
 
1504
  try {
1505
+ const useHyde = document.getElementById("arena-use-hyde") ? document.getElementById("arena-use-hyde").checked : false;
1506
+ const useReranking = document.getElementById("arena-use-reranking") ? document.getElementById("arena-use-reranking").checked : false;
1507
+ const filterLevel = document.getElementById("arena-filter-level") ? document.getElementById("arena-filter-level").value : "all";
1508
+ let whereClause = null;
1509
+ if (filterLevel === "parent") {
1510
+ whereClause = { "level": 0 };
1511
+ } else if (filterLevel === "child") {
1512
+ whereClause = { "level": 1 };
1513
+ }
1514
+
1515
  const res = await fetch("/api/compare", {
1516
  method: "POST",
1517
  headers: { "Content-Type": "application/json" },
 
1523
  model_b: domArena.modelB.value,
1524
  strategy_b: domArena.strategyB.value,
1525
  retrieval_mode: domArena.retrievalMode ? domArena.retrievalMode.value : "dense",
1526
+ use_hyde: useHyde,
1527
+ use_reranking: useReranking,
1528
+ metadata: whereClause,
1529
  }),
1530
  });
1531
 
 
1536
 
1537
  const data = await res.json();
1538
 
1539
+ // Render HyDE box if present
1540
+ const hydeText = document.getElementById("arena-hyde-text");
1541
+ if (hydeContainer && hydeText) {
1542
+ if (data.hypothetical_answer) {
1543
+ hydeText.textContent = `"${data.hypothetical_answer}"`;
1544
+ hydeContainer.style.display = "block";
1545
+ } else {
1546
+ hydeContainer.style.display = "none";
1547
+ }
1548
+ }
1549
+
1550
  // Render Column A
1551
  domArena.resultsA.innerHTML = data.results_a
1552
  .map(
1553
  (chunk, i) => `
1554
  <div class="query-result-item" style="border-left-color: var(--warning-color)">
1555
  <div class="query-result-header">
1556
+ <span class="rank-badge" style="color: var(--warning-color)">
1557
+ Rank ${i + 1}
1558
+ ${getRankShiftBadgeHtml(chunk.original_rank, i + 1)}
1559
+ </span>
1560
+ <span class="dist-badge">
1561
+ ${formatScoreHtml(chunk.score, chunk.original_score, domArena.retrievalMode ? domArena.retrievalMode.value : "dense", useReranking)}
1562
+ </span>
1563
  </div>
1564
  <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
1565
+ <div class="query-result-meta">
1566
+ <span>ID: ${chunk.id}</span>
1567
+ </div>
1568
  </div>
1569
  `,
1570
  )
 
1576
  (chunk, i) => `
1577
  <div class="query-result-item" style="border-left-color: var(--info-color)">
1578
  <div class="query-result-header">
1579
+ <span class="rank-badge" style="color: var(--info-color)">
1580
+ Rank ${i + 1}
1581
+ ${getRankShiftBadgeHtml(chunk.original_rank, i + 1)}
1582
+ </span>
1583
+ <span class="dist-badge">
1584
+ ${formatScoreHtml(chunk.score, chunk.original_score, domArena.retrievalMode ? domArena.retrievalMode.value : "dense", useReranking)}
1585
+ </span>
1586
  </div>
1587
  <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
1588
+ <div class="query-result-meta">
1589
+ <span>ID: ${chunk.id}</span>
1590
+ </div>
1591
  </div>
1592
  `,
1593
  )
frontend/index.html CHANGED
@@ -30,8 +30,12 @@
30
 
31
  <!-- Text Input -->
32
  <div class="config-section">
33
- <div class="config-section-title">Input Text</div>
 
 
 
34
  <div class="text-input-wrapper">
 
35
  <textarea
36
  id="text-input"
37
  class="text-input"
@@ -243,7 +247,7 @@
243
  <button id="btn-open-arena" class="btn-sm" style="background: var(--superman-red); color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; font-family: var(--font-sans);">⚔️ Arena Comparison</button>
244
  </div>
245
  <div class="query-input-wrapper">
246
- <select id="query-retrieval-mode" class="select-input" style="width: 110px; flex-shrink: 0; display: none;">
247
  <option value="dense" selected>Dense</option>
248
  <option value="sparse">Sparse</option>
249
  <option value="hybrid">Hybrid</option>
@@ -251,6 +255,24 @@
251
  <input type="text" id="query-input" placeholder="Type a concept query to fetch chunks (e.g. 'linear regression')..." />
252
  <button id="btn-query" class="btn-query">🔍 Query</button>
253
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  <div class="query-results-drawer" id="query-results-drawer" style="display: none;">
255
  <div class="drawer-header">
256
  <h3>Retrieved Context</h3>
@@ -310,7 +332,7 @@
310
  </div>
311
 
312
  <div class="arena-query-bar">
313
- <select id="arena-retrieval-mode" class="select-input" style="width: 110px; flex-shrink: 0; display: none;">
314
  <option value="dense" selected>Dense</option>
315
  <option value="sparse">Sparse</option>
316
  <option value="hybrid">Hybrid</option>
@@ -319,6 +341,31 @@
319
  <button class="btn-run" id="btn-arena-fight" style="width: 150px; flex-shrink: 0;">🔥 FIGHT!</button>
320
  </div>
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  <div class="arena-body">
323
  <div class="arena-columns">
324
  <!-- COLUMN A -->
 
30
 
31
  <!-- Text Input -->
32
  <div class="config-section">
33
+ <div class="config-section-title" style="display: flex; justify-content: space-between; align-items: center;">
34
+ <span>Input Text</span>
35
+ <button type="button" id="btn-upload-file" class="btn-sm" style="cursor: pointer; background: var(--superman-blue-muted); border: 1px solid var(--superman-blue); color: var(--superman-blue); padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: bold;">📎 Attach File</button>
36
+ </div>
37
  <div class="text-input-wrapper">
38
+ <input type="file" id="file-input" style="display: none;" />
39
  <textarea
40
  id="text-input"
41
  class="text-input"
 
247
  <button id="btn-open-arena" class="btn-sm" style="background: var(--superman-red); color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; font-family: var(--font-sans);">⚔️ Arena Comparison</button>
248
  </div>
249
  <div class="query-input-wrapper">
250
+ <select id="query-retrieval-mode" class="select-input" style="width: 110px; flex-shrink: 0;">
251
  <option value="dense" selected>Dense</option>
252
  <option value="sparse">Sparse</option>
253
  <option value="hybrid">Hybrid</option>
 
255
  <input type="text" id="query-input" placeholder="Type a concept query to fetch chunks (e.g. 'linear regression')..." />
256
  <button id="btn-query" class="btn-query">🔍 Query</button>
257
  </div>
258
+ <div class="query-options-row" style="display: flex; gap: var(--space-md); margin-top: var(--space-sm); font-size: 0.85rem; color: var(--text-secondary); align-items: center; flex-wrap: wrap;">
259
+ <label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; user-select: none;">
260
+ <input type="checkbox" id="query-use-hyde" style="cursor: pointer; width: 14px; height: 14px;" />
261
+ 🔮 Use HyDE
262
+ </label>
263
+ <label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; user-select: none;">
264
+ <input type="checkbox" id="query-use-reranking" style="cursor: pointer; width: 14px; height: 14px;" />
265
+ ⚡ Use Re-Ranking
266
+ </label>
267
+ <div style="display: flex; align-items: center; gap: var(--space-xs); margin-left: auto;">
268
+ <span style="font-size: 0.8rem; color: var(--text-tertiary);">Filter Level:</span>
269
+ <select id="query-filter-level" class="select-input" style="padding: 4px 8px; font-size: 0.78rem;">
270
+ <option value="all">All Levels</option>
271
+ <option value="parent">Only Parents (Lvl 0)</option>
272
+ <option value="child">Only Children (Lvl 1)</option>
273
+ </select>
274
+ </div>
275
+ </div>
276
  <div class="query-results-drawer" id="query-results-drawer" style="display: none;">
277
  <div class="drawer-header">
278
  <h3>Retrieved Context</h3>
 
332
  </div>
333
 
334
  <div class="arena-query-bar">
335
+ <select id="arena-retrieval-mode" class="select-input" style="width: 110px; flex-shrink: 0;">
336
  <option value="dense" selected>Dense</option>
337
  <option value="sparse">Sparse</option>
338
  <option value="hybrid">Hybrid</option>
 
341
  <button class="btn-run" id="btn-arena-fight" style="width: 150px; flex-shrink: 0;">🔥 FIGHT!</button>
342
  </div>
343
 
344
+ <div class="arena-options-row" style="display: flex; gap: var(--space-md); margin: 0 0 var(--space-md) 0; padding: 0 var(--space-xl); font-size: 0.85rem; color: var(--text-secondary); justify-content: flex-start; align-items: center; flex-wrap: wrap;">
345
+ <label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; user-select: none;">
346
+ <input type="checkbox" id="arena-use-hyde" style="cursor: pointer; width: 14px; height: 14px;" />
347
+ 🔮 Use HyDE
348
+ </label>
349
+ <label style="display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; user-select: none;">
350
+ <input type="checkbox" id="arena-use-reranking" style="cursor: pointer; width: 14px; height: 14px;" />
351
+ ⚡ Use Re-Ranking
352
+ </label>
353
+ <div style="display: flex; align-items: center; gap: var(--space-xs); margin-left: auto;">
354
+ <span style="font-size: 0.8rem; color: var(--text-tertiary);">Filter Level:</span>
355
+ <select id="arena-filter-level" class="select-input" style="padding: 4px 8px; font-size: 0.78rem;">
356
+ <option value="all">All Levels</option>
357
+ <option value="parent">Only Parents (Lvl 0)</option>
358
+ <option value="child">Only Children (Lvl 1)</option>
359
+ </select>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- Dynamic HyDE box in Arena -->
364
+ <div id="arena-hyde-container" style="display: none; margin: 0 var(--space-lg) var(--space-md) var(--space-lg); padding: var(--space-sm) var(--space-md); border-left: 4px solid var(--superman-blue); background-color: var(--superman-blue-muted); border-radius: var(--radius-sm);">
365
+ <div style="font-weight: bold; color: var(--superman-blue); font-size: 0.85rem; margin-bottom: 4px;">🔮 Generated Hypothetical Answer (HyDE)</div>
366
+ <div id="arena-hyde-text" style="font-style: italic; color: var(--text-primary); font-size: 0.85rem;"></div>
367
+ </div>
368
+
369
  <div class="arena-body">
370
  <div class="arena-columns">
371
  <!-- COLUMN A -->
frontend/styles.css CHANGED
@@ -750,6 +750,75 @@ input[type="range"]::-moz-range-thumb {
750
  position: relative;
751
  }
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  /* Lexical Heatmap Highlights */
754
  .query-result-text mark,
755
  .retrieved-chunk-text mark {
 
750
  position: relative;
751
  }
752
 
753
+ /* Query Result Footer Metadata */
754
+ .query-result-meta {
755
+ display: flex;
756
+ gap: var(--space-md);
757
+ margin-top: 6px;
758
+ font-size: 0.68rem;
759
+ color: var(--text-tertiary);
760
+ font-family: var(--font-mono);
761
+ border-top: 1px dashed var(--border-default);
762
+ padding-top: 4px;
763
+ }
764
+
765
+ /* Distance and Score Badges */
766
+ .dist-badge {
767
+ display: inline-flex;
768
+ align-items: center;
769
+ font-family: var(--font-mono);
770
+ font-size: 0.72rem;
771
+ background: var(--bg-surface-raised);
772
+ border: 1px solid var(--border-default);
773
+ padding: 2px 8px;
774
+ border-radius: 99px;
775
+ color: var(--text-secondary);
776
+ }
777
+
778
+ .score-badge-val {
779
+ font-weight: 600;
780
+ color: var(--superman-blue);
781
+ }
782
+
783
+ .score-badge-original {
784
+ font-size: 0.65rem;
785
+ color: var(--text-tertiary);
786
+ font-style: italic;
787
+ margin-left: 4px;
788
+ }
789
+
790
+ /* Rank Shift Badges */
791
+ .rank-shift-badge {
792
+ display: inline-flex;
793
+ align-items: center;
794
+ gap: 2px;
795
+ font-size: 0.72rem;
796
+ font-weight: 700;
797
+ padding: 2px 6px;
798
+ border-radius: 4px;
799
+ margin-left: 8px;
800
+ text-transform: uppercase;
801
+ letter-spacing: 0.02em;
802
+ }
803
+
804
+ .rank-shift-up {
805
+ color: #16a34a;
806
+ background-color: rgba(22, 163, 74, 0.08);
807
+ border: 1px solid rgba(22, 163, 74, 0.2);
808
+ }
809
+
810
+ .rank-shift-down {
811
+ color: #dc2626;
812
+ background-color: rgba(220, 38, 38, 0.08);
813
+ border: 1px solid rgba(220, 38, 38, 0.2);
814
+ }
815
+
816
+ .rank-shift-unchanged {
817
+ color: var(--text-tertiary);
818
+ background-color: var(--bg-surface-raised);
819
+ border: 1px solid var(--border-default);
820
+ }
821
+
822
  /* Lexical Heatmap Highlights */
823
  .query-result-text mark,
824
  .retrieved-chunk-text mark {
pyproject.toml CHANGED
@@ -16,4 +16,5 @@ dependencies = [
16
  "umap-learn>=0.5.12",
17
  "numpy>=2.4.4",
18
  "ollama>=0.6.2",
 
19
  ]
 
16
  "umap-learn>=0.5.12",
17
  "numpy>=2.4.4",
18
  "ollama>=0.6.2",
19
+ "flashrank>=0.2.10",
20
  ]
rag_test_corpus.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dataset 1: Superhero Origins and Vulnerabilities
2
+
3
+ Born Kal-El on the dying planet Krypton, the child was sent to Earth by his father Jor-El just before planetary destruction. He was raised as Clark Kent in Smallville, Kansas. Under Earth's yellow sun, his Kryptonian cells function like solar batteries, granting him superpowers: flight, heat vision, and invulnerability. However, he is vulnerable to radioactive fragments of his home planet.
4
+
5
+ Green Kryptonite is the most common form; it causes immediate physical pain, weakens his muscles, and will eventually kill him if exposure is prolonged. Red Kryptonite does not weaken him physically but alters his psychology, inducing wild mood swings, aggression, and apathy. Blue Kryptonite affects only Bizarro clones, stripping them of their powers, but is harmless to normal Kryptonians. Gold Kryptonite is highly dangerous, as it permanently strips a Kryptonian of their super-abilities by destroying their cellular solar absorption capability.
6
+
7
+ In his civilian identity, Clark Kent works as an investigative reporter in Metropolis. He works at the Daily Planet, a major metropolitan newspaper known for its giant golden globe symbol atop its building. His editor-in-chief is the hot-tempered Perry White, who demands hard-hitting journalistic integrity. Clark shares a desk office space with Lois Lane, a star reporter whom he eventually marries, and Jimmy Olsen, a young photographer famous for his signature signal-watch that emits a high-frequency sound only Superman can hear.
8
+
9
+ Alexander Joseph Luthor, CEO of LexCorp, is a brilliant billionaire scientist obsessed with exposing Superman as a threat to humanity. LexCorp occupies a high-tech corporate campus in downtown Metropolis, featuring sub-level research laboratories.
10
+
11
+ Luthor's primary weapon development project is the "Xenon-based Solar Radiation Simulator" (Project XSRS). This high-power apparatus emits synthetic red solar radiation, mimicking Krypton's red giant star, Rao. When Superman enters the simulator's projection field, his yellow sun charge is neutralized, temporarily rendering him as weak as an ordinary human. This allows Luthor's armored strike forces to engage him directly without facing super-strength.
12
+
13
+ # Dataset 2: Machine Learning and Linear Regression
14
+
15
+ Linear regression is a foundational supervised learning algorithm used to model the relationship between a dependent scalar variable Y and one or more explanatory variables denoted X. When there is one explanatory variable, it is called simple linear regression; for more than one, it is called multiple linear regression. The algorithm assumes a linear relationship and estimates coefficients to construct a predictive line.
16
+
17
+ The model is defined by the equation Y = beta_0 + beta_1 * X + epsilon, where beta_0 represents the Y-intercept, beta_1 represents the slope coefficient (indicating the change in Y per unit change in X), and epsilon represents the residual error. The goal of fitting a regression line is to find the values of beta that minimize the differences between the predicted values and the actual observed data points.
18
+
19
+ To find the optimal coefficients, the algorithm utilizes Ordinary Least Squares (OLS). OLS minimizes the Sum of Squared Residuals (SSR), which represents the vertical distances between the observed points and the fitted line. By squaring the residuals, the algorithm penalizes larger errors more heavily and prevents positive and negative errors from canceling each other out.
20
+
21
+ Gradient Descent is an alternative optimization algorithm used to find the coefficients, particularly in high-dimensional settings. It starts with random values for beta and iteratively adjusts them in the direction of the steepest descent of the loss function (the negative gradient). The size of the steps taken in each iteration is determined by the learning rate parameter, which must be tuned carefully to prevent overshooting or extremely slow convergence.
22
+
23
+ Overfitting occurs when a linear model fits the training data too closely, capturing random noise rather than the underlying pattern. This leads to poor generalization on unseen data. To mitigate this, regularization techniques are introduced: Ridge Regression (L2 regularization) adds a penalty term proportional to the square of the coefficients, while Lasso Regression (L1 regularization) adds a penalty proportional to the absolute values of the coefficients, effectively driving some coefficient weights to zero to perform automatic feature selection.
24
+
25
+ # Dataset 3: Corporate Remote Work and Cybersecurity Policy
26
+
27
+ This policy defines the security configurations and operational guidelines required for all employees working remotely. Remote access to the corporate intranet is permitted only through the Unified Endpoint Security VPN (UES-VPN), utilizing mandatory multi-factor authentication (MFA). Employees are strictly prohibited from using public, unsecured Wi-Fi networks unless a secondary virtual private network connection is active.
28
+
29
+ All corporate laptops must run the designated Enterprise Threat Detection agent (ETD v4.2) at all times. The agent performs real-time heuristic scans of local file storage and intercepts unauthorized external network requests. System patches and security updates are pushed automatically every Wednesday at 03:00 UTC, and endpoints must remain powered on to receive these critical updates.
30
+
31
+ Data classification guidelines dictate that any document containing personally identifiable information (PII) or proprietary algorithm designs must be tagged as "Confidential - Level 3." These files must be stored exclusively in encrypted, access-controlled SharePoint repositories. Sharing Level 3 documents via public channels, standard email attachments, or external messaging platforms is a high-severity violation subject to disciplinary action.
32
+
33
+ Incident response protocols require that any suspected endpoint compromise or malware detection event be reported to the security operations center (SOC) within 15 minutes of discovery. Endpoints flagged as potentially compromised will be automatically isolated from the internal network by the active directory controller, preventing lateral movement across corporate segments while forensic teams analyze the logs.
34
+
35
+ # Dataset 4: Roman History and Infrastructure
36
+
37
+ The Roman Empire's expansion and stability relied heavily on its mastery of engineering and physical infrastructure. Key to Roman connectivity was the construction of paved stone roads. These roadways featured deep gravel beds for drainage, stone borders, and milestone markers indicating the distance to the local regional capital.
38
+
39
+ Aqueducts were built to transport clean mountain water directly to metropolitan bathhouses and public fountains. These conduits operated solely on gravity, requiring precise calculations of gradient channels. When crossing deep valleys, Roman engineers constructed multi-tier stone arch bridges, such as the Pont du Gard, to maintain the steady flow of water without needing mechanical pumps.
40
+
41
+ Concrete, or "opus caementicium," was a revolutionary Roman invention. By combining volcanic ash (pozzolana) with lime and volcanic tuff, builders created a fast-setting, water-resistant binding mortar. This concrete allowed for the construction of massive domes, such as the Pantheon in Rome, which features a coffered unreinforced concrete ceiling spanning 43 meters.
42
+
43
+ Military legions were responsible for constructing fortified outposts, or "castra," along the empire's borders. These camps followed a standardized grid layout, containing central barracks, silos, and defensive palisades. Along the northern borders, walls like Hadrian's Wall stretched across miles of open terrain to isolate Roman territories from tribal raids.
44
+
45
+ # Dataset 5: Molecular Biology and DNA Replication
46
+
47
+ Deoxyribonucleic acid (DNA) is the double-stranded helical molecule storing the genetic blueprint of cellular life. The structure consists of nucleotide bases (adenine, thymine, cytosine, and guanine) bound to a deoxyribose-phosphate backbone. DNA replication is semiconservative, meaning each daughter molecule contains one original parent strand and one newly synthesized strand.
48
+
49
+ The replication process begins at specific chromosomal regions known as the origin of replication. The enzyme DNA Helicase unwinds the double helix, breaking hydrogen bonds between complementary base pairs. This creates a Y-shaped replication fork, exposing single strands of template DNA for synthesis.
50
+
51
+ To prevent the exposed single strands from re-annealing, Single-Strand Binding Proteins (SSBs) coat the open DNA. An enzyme called RNA Primase then synthesizes short RNA primers, providing the free 3'-hydroxyl group required for DNA Polymerase III to begin adding complementary DNA nucleotides.
52
+
53
+ DNA replication is directional, occurring only in the 5'-to-3' direction. On the leading strand, synthesis occurs continuously toward the replication fork. On the lagging strand, synthesis runs discontinuously away from the fork, generating short segments called Okazaki fragments. These fragments are later sealed into a continuous strand by the enzyme DNA Ligase.
54
+
55
+ # Dataset 6: Monetary Policy and Financial Markets
56
+
57
+ Monetary policy refers to the actions taken by a nation's central bank to control the money supply and influence macroeconomic objectives, such as price stability and maximum employment. The primary tool used is the adjustment of the target interbank lending rate (e.g., the Federal Funds Rate in the United States).
58
+
59
+ When inflation rises above target levels, the central bank implements contractionary monetary policy. By raising interest rates, borrowing costs increase for commercial banks, which in turn raises interest rates for business loans and mortgages. This higher cost of capital dampens consumer spending and business investment, cooling the overall economic expansion.
60
+
61
+ Conversely, during economic recessions, expansionary policy is deployed. The central bank cuts interest rates to lower borrowing costs, encouraging consumer credit usage and business expansion. In extreme scenarios where rates approach zero, central banks use Quantitative Easing (QE), purchasing large quantities of long-term government bonds to inject liquidity directly into the financial system.
62
+
63
+ Financial markets react dynamically to monetary decisions. Bond yields generally rise in lockstep with central bank rate hikes, causing bond prices to fall. Equity markets often experience volatility during contractionary cycles, as higher discount rates reduce the present value of future corporate earnings, making stocks less attractive compared to fixed-income assets.
64
+
65
+ # Dataset 7: Classical Culinary Arts and Sourdough Fermentation
66
+
67
+ The art of baking sourdough bread relies entirely on natural wild fermentation rather than commercial baker's yeast. A sourdough starter, or "levain," is a symbiotic culture of wild yeasts (primarily Candida humilis) and lactic acid bacteria (such as Lactobacillus sanfranciscensis) cultivated from flour and water.
68
+
69
+ Sourdough preparation begins with autolyse, the mixing of flour and water to hydrate proteins and activate naturally occurring amylase enzymes. This resting phase initiates the development of gluten strands before the addition of salt and the levain culture.
70
+
71
+ During bulk fermentation, the dough undergoes folding cycles to build structural elasticity and trap carbon dioxide gas. The lactic acid bacteria produce organic acids, giving the dough its signature sour flavor profile and lowering the pH, which improves gluten strength and increases shelf stability.
72
+
73
+ The baking phase requires a high-heat environment, typically inside a preheated cast-iron Dutch oven. The trapped steam inside the closed vessel keeps the dough's surface moist, allowing the loaf to expand fully (the "oven spring") before the cover is removed to let the crust brown through the Maillard reaction.
74
+
75
+ # Dataset 8: Astronomy and Star Lifecycles
76
+
77
+ Stars form inside dense interstellar clouds of gas and dust known as molecular clouds or stellar nurseries. Gravity causes localized pockets in these clouds to collapse under their own mass, heating the core until nuclear fusion begins, transitioning the object into a main-sequence star.
78
+
79
+ Main-sequence stars generate energy by fusing hydrogen atoms into helium in their cores. The outbound radiation pressure from this fusion process perfectly balances the inbound gravitational force, establishing a state of hydrostatic equilibrium that keeps the star stable for millions or billions of years.
80
+
81
+ When a low-mass star runs out of hydrogen, its core contracts and heats up, causing the outer layers to expand and cool, transforming the star into a red giant. Eventually, the outer layers are ejected, forming a planetary nebula, leaving behind a hot, dense core called a white dwarf that slowly cools over time.
82
+
83
+ Massive stars end their lives in dramatic supernova explosions. When their cores can no longer support nuclear fusion against gravity, they collapse in milliseconds, creating a shockwave that ejects the star's outer envelope. The remaining compressed core becomes either a highly magnetic neutron star or a gravitational black hole.
84
+
85
+ # Dataset 9: Psychology, Memory, and Sleep Cycles
86
+
87
+ Human memory is categorized into distinct stages: sensory memory, working memory, and long-term memory. Working memory holds transient information for immediate processing, while long-term memory stores vast amounts of information indefinitely through structural changes in synaptic connections, a process known as consolidation.
88
+
89
+ Sleep plays a critical role in memory consolidation. During sleep, the brain actively replays neural activity from the day, transferring information from the temporary storage of the hippocampus to the permanent storage of the neocortex.
90
+
91
+ The sleep cycle alternates between Non-Rapid Eye Movement (NREM) and Rapid Eye Movement (REM) sleep. NREM sleep is divided into light sleep and slow-wave sleep (SWS). Slow-wave sleep is characterized by synchronized delta brain waves, which are crucial for consolidating declarative memories (facts and events).
92
+
93
+ REM sleep is characterized by rapid eye movements, high brain activity, and muscle atonia. This stage is associated with intense dreaming and is critical for consolidating procedural memories (skills and tasks) and processing emotional experiences.
uv.lock CHANGED
@@ -326,6 +326,22 @@ wheels = [
326
  { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
327
  ]
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  [[package]]
330
  name = "flatbuffers"
331
  version = "25.12.19"
@@ -1622,6 +1638,7 @@ source = { virtual = "." }
1622
  dependencies = [
1623
  { name = "chromadb" },
1624
  { name = "fastapi" },
 
1625
  { name = "httpx" },
1626
  { name = "langchain-text-splitters" },
1627
  { name = "nltk" },
@@ -1637,6 +1654,7 @@ dependencies = [
1637
  requires-dist = [
1638
  { name = "chromadb", specifier = ">=1.0.0" },
1639
  { name = "fastapi", specifier = ">=0.115.0" },
 
1640
  { name = "httpx", specifier = ">=0.28.0" },
1641
  { name = "langchain-text-splitters", specifier = ">=1.1.2" },
1642
  { name = "nltk", specifier = ">=3.9.4" },
 
326
  { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
327
  ]
328
 
329
+ [[package]]
330
+ name = "flashrank"
331
+ version = "0.2.10"
332
+ source = { registry = "https://pypi.org/simple" }
333
+ dependencies = [
334
+ { name = "numpy" },
335
+ { name = "onnxruntime" },
336
+ { name = "requests" },
337
+ { name = "tokenizers" },
338
+ { name = "tqdm" },
339
+ ]
340
+ sdist = { url = "https://files.pythonhosted.org/packages/55/1f/176cb4a857a70c3538f637e19389ab6aed21548a1ba1d1424fccc8bba108/FlashRank-0.2.10.tar.gz", hash = "sha256:f8f82a25c32fdfc668a09dc4089421d6aab8e7f71308424b541f40bb3f01d9db", size = 18905, upload-time = "2025-01-06T13:33:01.657Z" }
341
+ wheels = [
342
+ { url = "https://files.pythonhosted.org/packages/ec/99/72639cc1c9221c5bc77a2df1c2d352fe11965553bdf7d3e0856e7fcc8fd6/FlashRank-0.2.10-py3-none-any.whl", hash = "sha256:5d3272ae657d793c132d1e7917ed9e2adf49e0e1c60735583a67b051c6f0434a", size = 14511, upload-time = "2025-01-06T13:32:59.42Z" },
343
+ ]
344
+
345
  [[package]]
346
  name = "flatbuffers"
347
  version = "25.12.19"
 
1638
  dependencies = [
1639
  { name = "chromadb" },
1640
  { name = "fastapi" },
1641
+ { name = "flashrank" },
1642
  { name = "httpx" },
1643
  { name = "langchain-text-splitters" },
1644
  { name = "nltk" },
 
1654
  requires-dist = [
1655
  { name = "chromadb", specifier = ">=1.0.0" },
1656
  { name = "fastapi", specifier = ">=0.115.0" },
1657
+ { name = "flashrank", specifier = ">=0.2.10" },
1658
  { name = "httpx", specifier = ">=0.28.0" },
1659
  { name = "langchain-text-splitters", specifier = ">=1.1.2" },
1660
  { name = "nltk", specifier = ">=3.9.4" },