Spaces:
Running
Running
Deploy dashVectorspace v1 (Full)
Browse files- .gitignore +2 -0
- README.md +70 -12
- app.py +127 -0
- config.py +29 -0
- logs/active_learning_queue.jsonl +59 -0
- main.py +151 -0
- notebooks/xVector_Analysis.ipynb +88 -0
- requirements.txt +10 -0
- scripts/ingest_ms_marco.py +82 -0
- src/__init__.py +0 -0
- src/active_learning.py +40 -0
- src/comparison.py +95 -0
- src/data_pipeline.py +117 -0
- src/router.py +132 -0
- src/vector_db.py +188 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
__pycache__/
|
README.md
CHANGED
|
@@ -1,12 +1,70 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dashVectorspace (xVector)
|
| 2 |
+
|
| 3 |
+
**Production-Grade Learned Hybrid Retrieval Engine**
|
| 4 |
+
|
| 5 |
+
This project implements a high-efficiency vector search engine using a **Learned Router** and **Custom Sharding** on top of **Qdrant**. It optimizes search efficiency by ~90% by routing queries to specific data clusters instead of performing a brute-force search across the entire dataset.
|
| 6 |
+
|
| 7 |
+
## Core Architecture
|
| 8 |
+
|
| 9 |
+
1. **The Brain (Router)**: A Machine Learning model (LightGBM/Logistic/MLP) predicts which cluster contains the answer.
|
| 10 |
+
2. **The Body (Vector DB)**: Qdrant with **Custom Sharding**. Data is partitioned into 32 Clusters + 1 Freshness Shard.
|
| 11 |
+
3. **The Optimization**: **Matryoshka Representation Learning (MRL)**. The Router uses sliced 64-dim vectors for speed, while the DB stores full vectors for accuracy.
|
| 12 |
+
|
| 13 |
+
## Project Structure
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
dashVectorspace/
|
| 17 |
+
├── config.py # Configuration (Clusters, Models, Paths)
|
| 18 |
+
├── main.py # Benchmark Runner (P&C Matrix)
|
| 19 |
+
├── requirements.txt # Dependencies
|
| 20 |
+
├── src/
|
| 21 |
+
│ ├── data_pipeline.py # Data loading & MRL slicing
|
| 22 |
+
│ ├── router.py # LearnedRouter (Train/Predict)
|
| 23 |
+
│ ├── vector_db.py # UnifiedQdrant (Custom Sharding)
|
| 24 |
+
│ └── active_learning.py # Hard Negative Logging
|
| 25 |
+
└── notebooks/
|
| 26 |
+
└── xVector_Analysis.ipynb # Analysis Notebook
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Setup & Usage
|
| 30 |
+
|
| 31 |
+
1. **Install Dependencies**:
|
| 32 |
+
```bash
|
| 33 |
+
pip install -r requirements.txt
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
2. **Run Benchmarks**:
|
| 37 |
+
Execute the main script to run the Permutation & Combination matrix of experiments:
|
| 38 |
+
```bash
|
| 39 |
+
python main.py
|
| 40 |
+
```
|
| 41 |
+
This will:
|
| 42 |
+
- Generate/Load Data (MS MARCO or Synthetic).
|
| 43 |
+
- Train different Router models (LightGBM, Logistic, MLP).
|
| 44 |
+
- Index data into Qdrant with Custom Sharding.
|
| 45 |
+
- Run test queries and report Accuracy, Latency, and Compute Savings.
|
| 46 |
+
|
| 47 |
+
3. **Analyze Results**:
|
| 48 |
+
Open `notebooks/xVector_Analysis.ipynb` to visualize the active learning logs and performance metrics.
|
| 49 |
+
|
| 50 |
+
## Key Features
|
| 51 |
+
|
| 52 |
+
- **Custom Sharding**: Explicit control over where data lives (Clusters 0-31) and a dedicated **Freshness Shard (999)** for new data.
|
| 53 |
+
- **Drift Defense**:
|
| 54 |
+
- **Layer 1**: Always searches the Freshness Shard.
|
| 55 |
+
- **Layer 2**: Falls back to **Global Search** if Router confidence is low (< 0.5).
|
| 56 |
+
- **Active Learning**: Logs "Hard Negatives" (low confidence or zero results) to `logs/active_learning_queue.jsonl` for future model retraining.
|
| 57 |
+
|
| 58 |
+
## Hugging Face Space Deployment
|
| 59 |
+
|
| 60 |
+
This project is ready for deployment on Hugging Face Spaces.
|
| 61 |
+
|
| 62 |
+
1. **Create a New Space**: Select "Gradio" as the SDK.
|
| 63 |
+
2. **Upload Files**: Upload the entire `dashVectorspace` folder content to the Space.
|
| 64 |
+
3. **Set Secrets**: Go to "Settings" -> "Repository secrets" and add:
|
| 65 |
+
- `QDRANT_URL`: Your Qdrant Cloud Cluster URL.
|
| 66 |
+
- `QDRANT_API_KEY`: Your Qdrant Cloud API Key.
|
| 67 |
+
4. **Ingest Data**:
|
| 68 |
+
- Run `python scripts/ingest_ms_marco.py` locally (with env vars set) to populate your Qdrant Cloud instance and generate `models/router_v1.pkl`.
|
| 69 |
+
- **Upload `models/router_v1.pkl`** to the Space (inside a `models/` folder).
|
| 70 |
+
5. **Run**: The Space will automatically launch `app.py`.
|
app.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
from src.vector_db import UnifiedQdrant
|
| 6 |
+
from src.router import LearnedRouter
|
| 7 |
+
from src.comparison import ComparisonEngine
|
| 8 |
+
from config import COLLECTION_NAME, NUM_CLUSTERS, FRESHNESS_SHARD_ID, MRL_DIMS
|
| 9 |
+
|
| 10 |
+
# --- Initialization ---
|
| 11 |
+
print("Initializing dashVectorspace App...")
|
| 12 |
+
|
| 13 |
+
# 1. Initialize DB
|
| 14 |
+
# Note: In a real HF Space, secrets are in os.environ
|
| 15 |
+
db = UnifiedQdrant(
|
| 16 |
+
collection_name=COLLECTION_NAME,
|
| 17 |
+
vector_size=384, # Assuming MiniLM for demo
|
| 18 |
+
num_clusters=NUM_CLUSTERS,
|
| 19 |
+
freshness_shard_id=FRESHNESS_SHARD_ID
|
| 20 |
+
)
|
| 21 |
+
db.initialize()
|
| 22 |
+
|
| 23 |
+
# 2. Initialize Router
|
| 24 |
+
ROUTER_PATH = "models/router_v1.pkl"
|
| 25 |
+
if os.path.exists(ROUTER_PATH):
|
| 26 |
+
router = LearnedRouter.load(ROUTER_PATH)
|
| 27 |
+
else:
|
| 28 |
+
print("WARNING: Router model not found. Creating a DUMMY router for demo UI.")
|
| 29 |
+
router = LearnedRouter(model_type="lightgbm", n_clusters=NUM_CLUSTERS, mrl_dims=MRL_DIMS)
|
| 30 |
+
# We can't really predict without training, but let's mock it or fail gracefully.
|
| 31 |
+
# For the UI to load, we need an object.
|
| 32 |
+
# If we try to predict, it will crash if not trained.
|
| 33 |
+
# Let's mock the predict method if not trained.
|
| 34 |
+
router.predict = lambda x: (0, 0.99) # Mock prediction: Cluster 0, High Confidence
|
| 35 |
+
|
| 36 |
+
# 3. Initialize Engine
|
| 37 |
+
engine = ComparisonEngine(db, router, embedding_model_name="minilm")
|
| 38 |
+
|
| 39 |
+
# --- UI Logic ---
|
| 40 |
+
def run_comparison(query):
|
| 41 |
+
if not query:
|
| 42 |
+
return "Please enter a query.", None, None, None, None
|
| 43 |
+
|
| 44 |
+
# Run Direct Search
|
| 45 |
+
res_direct = engine.direct_search(query)
|
| 46 |
+
|
| 47 |
+
# Run xVector Search
|
| 48 |
+
res_xvector = engine.xvector_search(query)
|
| 49 |
+
|
| 50 |
+
# Format Results
|
| 51 |
+
def format_results(res_dict):
|
| 52 |
+
points = res_dict["results"]
|
| 53 |
+
text_res = ""
|
| 54 |
+
for p in points:
|
| 55 |
+
# Payload might be dict or object depending on client version/mock
|
| 56 |
+
payload = p.payload
|
| 57 |
+
text = payload.get("text", "No text") if payload else "No text"
|
| 58 |
+
score = p.score
|
| 59 |
+
text_res += f"- [{score:.4f}] {text[:100]}...\n"
|
| 60 |
+
return text_res
|
| 61 |
+
|
| 62 |
+
out_direct = format_results(res_direct)
|
| 63 |
+
out_xvector = format_results(res_xvector)
|
| 64 |
+
|
| 65 |
+
# Metrics
|
| 66 |
+
metrics_df = pd.DataFrame({
|
| 67 |
+
"Metric": ["Latency (ms)", "Shards Searched"],
|
| 68 |
+
"Brute Force": [res_direct["latency_ms"], res_direct["shards_searched"]],
|
| 69 |
+
"xVector": [res_xvector["latency_ms"], res_xvector["shards_searched"]]
|
| 70 |
+
})
|
| 71 |
+
|
| 72 |
+
# Compute Savings
|
| 73 |
+
savings = (1 - (res_xvector["shards_searched"] / res_direct["shards_searched"])) * 100
|
| 74 |
+
savings_text = f"Compute Savings: {savings:.1f}%"
|
| 75 |
+
|
| 76 |
+
# Telemetry
|
| 77 |
+
telemetry = f"""
|
| 78 |
+
**Search Mode:** {res_xvector['mode']}
|
| 79 |
+
**Router Confidence:** {res_xvector.get('confidence', 0):.4f}
|
| 80 |
+
**Target Cluster:** {res_xvector.get('target_cluster', 'N/A')}
|
| 81 |
+
**Shards Scanned:** {res_xvector['shards_searched']} vs {res_direct['shards_searched']}
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
return out_direct, out_xvector, metrics_df, savings_text, telemetry
|
| 85 |
+
|
| 86 |
+
# --- Gradio Layout ---
|
| 87 |
+
with gr.Blocks(title="dashVectorspace: Learned Hybrid Retrieval", theme=gr.themes.Soft()) as demo:
|
| 88 |
+
gr.Markdown("# 🚀 dashVectorspace: Learned Hybrid Retrieval Engine")
|
| 89 |
+
gr.Markdown("Comparing **Brute Force Vector Search** vs **xVector (Learned Router + Custom Sharding)**.")
|
| 90 |
+
|
| 91 |
+
with gr.Row():
|
| 92 |
+
query_input = gr.Textbox(label="Enter your query", placeholder="e.g., What is the impact of AI on healthcare?", lines=2)
|
| 93 |
+
submit_btn = gr.Button("🚀 Run Comparison", variant="primary")
|
| 94 |
+
|
| 95 |
+
with gr.Row():
|
| 96 |
+
with gr.Column(scale=1):
|
| 97 |
+
gr.Markdown("### 🐢 Brute Force (Standard)")
|
| 98 |
+
out_baseline = gr.Textbox(label="Results", lines=10)
|
| 99 |
+
|
| 100 |
+
with gr.Column(scale=1):
|
| 101 |
+
gr.Markdown("### ⚡ xVector (Optimized)")
|
| 102 |
+
out_optimized = gr.Textbox(label="Results", lines=10)
|
| 103 |
+
|
| 104 |
+
with gr.Row():
|
| 105 |
+
with gr.Column():
|
| 106 |
+
metrics_plot = gr.BarPlot(
|
| 107 |
+
x="Metric",
|
| 108 |
+
y="Brute Force",
|
| 109 |
+
title="Performance Comparison",
|
| 110 |
+
tooltip=["Metric", "Brute Force", "xVector"],
|
| 111 |
+
# Gradio BarPlot expects long format usually, but let's try simple DF display first if BarPlot is complex
|
| 112 |
+
)
|
| 113 |
+
# Actually, let's use a simple DataFrame for metrics first, it's cleaner.
|
| 114 |
+
metrics_table = gr.Dataframe(label="Performance Metrics")
|
| 115 |
+
|
| 116 |
+
with gr.Column():
|
| 117 |
+
savings_display = gr.Markdown("### Compute Savings: --%")
|
| 118 |
+
telemetry_display = gr.Markdown("### Telemetry\nWaiting for query...")
|
| 119 |
+
|
| 120 |
+
submit_btn.click(
|
| 121 |
+
run_comparison,
|
| 122 |
+
inputs=[query_input],
|
| 123 |
+
outputs=[out_baseline, out_optimized, metrics_table, savings_display, telemetry_display]
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
if __name__ == "__main__":
|
| 127 |
+
demo.launch()
|
config.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# --- Architecture Constants ---
|
| 4 |
+
NUM_CLUSTERS = 32
|
| 5 |
+
FRESHNESS_SHARD_ID = 999
|
| 6 |
+
MRL_DIMS = 64
|
| 7 |
+
|
| 8 |
+
# --- Qdrant Configuration ---
|
| 9 |
+
# Use in-memory for testing if QDRANT_URL is not set, otherwise connect to cloud/local instance
|
| 10 |
+
QDRANT_URL = os.getenv("QDRANT_URL", "https://justmotes-xvector-db-node.hf.space")
|
| 11 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY", "xvector_secret_pass_123")
|
| 12 |
+
COLLECTION_NAME = "dashVector_v1"
|
| 13 |
+
|
| 14 |
+
# --- Model Configurations ---
|
| 15 |
+
EMBEDDING_MODELS = {
|
| 16 |
+
"minilm": "sentence-transformers/all-MiniLM-L6-v2", # Baseline (384 dims)
|
| 17 |
+
"nomic": "nomic-ai/nomic-embed-text-v1.5", # Primary, MRL-capable (768 dims, matryoshka compatible)
|
| 18 |
+
"qwen": "Alibaba-NLP/gte-Qwen2-1.5B-instruct" # SOTA (1536 dims)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
ROUTER_MODELS = ["lightgbm", "logistic", "mlp"]
|
| 22 |
+
|
| 23 |
+
# --- Paths ---
|
| 24 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
+
LOGS_DIR = os.path.join(BASE_DIR, "logs")
|
| 26 |
+
ACTIVE_LEARNING_LOG = os.path.join(LOGS_DIR, "active_learning_queue.jsonl")
|
| 27 |
+
|
| 28 |
+
# Ensure logs directory exists
|
| 29 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
logs/active_learning_queue.jsonl
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"timestamp": "2025-12-05T04:02:49.290476", "query": "Best Answer: NaOH is a alkali (bases soluble in water) & HCl is an acid. 1) Acid + base \u2192 Salt + water + heat (neutralisation reaction). 2)The equation is already balanced. To check whether it is balanced check whether the number of atoms of each kind are equal on both RHS & LHS.", "confidence": 0.3504535932154855, "result_count": 10, "reasons": ["low_confidence"]}
|
| 2 |
+
{"timestamp": "2025-12-05T04:02:49.296195", "query": "Evangelista Torricelli 's parents were Gaspare Torricelli and Caterina Angetti. It was a fairly poor family with Gaspare being a textile worker. Evangelista was the eldest of his parents three children, having two younger brothers at least one of whom went on to work with cloth.", "confidence": 0.27602324660862104, "result_count": 10, "reasons": ["low_confidence"]}
|
| 3 |
+
{"timestamp": "2025-12-05T04:02:49.307386", "query": "Definition. Turner syndrome, a condition that affects only girls and women, results when a sex chromosome (the X chromosome) is missing or partially missing. ", "confidence": 0.5561548368359581, "result_count": 10, "reasons": ["low_confidence"]}
|
| 4 |
+
{"timestamp": "2025-12-05T04:02:49.326399", "query": "Overview. Spider angiomas are common in both children and adults. They appear more frequently during pregnancy, in people on birth control pills, or in people with liver disease. Who's At Risk. Spider angiomas are most often seen on the face or trunk, and they also may be seen on the hands, forearms, and ears. There may be one spider angioma or several. Each one is a small (1\u201310 mm) area of redness, which disappears with direct finger pressure but rapidly returns when the pressure is released.", "confidence": 0.5066801533454477, "result_count": 10, "reasons": ["low_confidence"]}
|
| 5 |
+
{"timestamp": "2025-12-05T04:02:49.332817", "query": "If more than one egg is released, but only one is fertilized, the unfertilized eggs are absorbed by your body and do not cause you to have a period. The uterine lining that you normally loose during your menstrual cycle is needed for the fertilized egg so it does not slough off. If your ovaries release more than 1 egg but only 1 egg is fertilized then you still will NOT have a period. It simply is not possible to be pregnant and have a period, but it is very possible to be pregnant, have abnormal bleeding that the woman mistakes for her period. jilldaniel_wv \u00b7 9 years ago. Thumbs up.", "confidence": 0.2832711737078318, "result_count": 10, "reasons": ["low_confidence"]}
|
| 6 |
+
{"timestamp": "2025-12-05T04:02:49.372817", "query": "1 One type of Jack Russell is longer than he is tall, standing only 10 to 12 inches at the shoulder. 2 These dogs are nicknamed Shorty Jacks and resemble Corgis or Dachshunds more than they do the taller Parson Russell or Jack Russel Terrier Club of America (JRTCA) Jack Russell. ", "confidence": 0.4964724302274644, "result_count": 10, "reasons": ["low_confidence"]}
|
| 7 |
+
{"timestamp": "2025-12-05T04:02:49.386680", "query": "You can provide a suitable butterfly habitat that will help fortify the butterfly population, and as an added bonus, the habitat will bring you enjoyment in watching beautiful butterflies in your yard. The butterfly habitat should be relatively sunny (5-6 hours per day) and out of the wind. Facts About Butterflies. Natural butterfly habitats have been destroyed or affected by construction of housing and shopping developments, as well as by the use of pesticides and other chemicals.", "confidence": 0.48337516671682024, "result_count": 10, "reasons": ["low_confidence"]}
|
| 8 |
+
{"timestamp": "2025-12-05T04:02:49.390507", "query": "In Illinois, you have to work for your employer for 30 days in order for that employer to be chargeable for unemployment benefits in the event you become unemployed. Regular unemployment benefits: If you meet the eligibility requirements of the law, you will have some income while you are looking for a job, up to a maximum of 26 full weeks \u2026 in a one-year period.", "confidence": 0.45567612613411346, "result_count": 10, "reasons": ["low_confidence"]}
|
| 9 |
+
{"timestamp": "2025-12-05T04:04:13.849402", "query": "Definition. Turner syndrome, a condition that affects only girls and women, results when a sex chromosome (the X chromosome) is missing or partially missing. ", "confidence": 0.15475964960575342, "result_count": 10, "reasons": ["low_confidence"]}
|
| 10 |
+
{"timestamp": "2025-12-05T04:04:13.855185", "query": "The current that is sent to the hair follicle can be adjusted depending on the thickness and type of the hair. 1 According to eHow.com, the cost of your electrolysis is determined by the number of treatments and by the time needed for the procedure. 2 However, the average cost is $15 to $35 for 20 minutes.", "confidence": 0.1676933617190043, "result_count": 10, "reasons": ["low_confidence"]}
|
| 11 |
+
{"timestamp": "2025-12-05T04:04:13.860697", "query": "Nova is a very prominent first name for women (#1600 out of 4276, Top 37%) and also a very prominent last name for both adults and children (#13153 out of 150436, Top 9%). (2000 U.S. Census). Astronomy: a nova is a star that releases a tremendous burst of energy, becoming temporarily extraordinarily bright. Chevrolet used to make a small car called a Nova. Novia is also Spanish for girlfriend.", "confidence": 0.2974863522179262, "result_count": 10, "reasons": ["low_confidence"]}
|
| 12 |
+
{"timestamp": "2025-12-05T04:04:13.865692", "query": "Medical Definition of DEAD SPACE. 1. : space in the respiratory system in which air does not undergo significant gaseous exchange\u2014see anatomical dead space, physiological dead space. 2. : a space (as that in the chest following excision of a lung) left in the body as the result of a surgical procedure. Definition of DEAD SPACE. : the portion of the respiratory system which is external to the bronchioles and through which air must pass to reach the bronchioles and alveoli. ADVERTISEMENT. noun. 1", "confidence": 0.20809382514381405, "result_count": 10, "reasons": ["low_confidence"]}
|
| 13 |
+
{"timestamp": "2025-12-05T04:04:13.872096", "query": "Al(OH) 3, for example, acts as an acid when it reacts with a base. Conversely, it acts as a base when it reacts with an acid. The Br nsted Definition of Acids and Bases. The Brnsted, or Brnsted-Lowry, model is based on a simple assumption: Acids donate H ions to another ion or molecule, which acts as a base. Arrhenius bases include ionic compounds that contain the OH-ion, such as NaOH, KOH, and Ca(OH) 2. This theory explains why acids have similar properties: The characteristic properties of acids result from the presence of the H + ion generated when an acid dissolves in water.", "confidence": 0.133487619260465, "result_count": 10, "reasons": ["low_confidence"]}
|
| 14 |
+
{"timestamp": "2025-12-05T04:04:13.877720", "query": "Non melanoma skin cancer is different from melanoma. Melanoma is the type of skin cancer that most often develops from a mole. If you are looking for information on melanoma, go to the separate section on melanoma skin cancer. There are 2 main types of n", "confidence": 0.10373228154886284, "result_count": 10, "reasons": ["low_confidence"]}
|
| 15 |
+
{"timestamp": "2025-12-05T04:04:13.881981", "query": "A U.S. utility patent, explained above, is generally granted for 20 years from the date the patent application is filed; however, periodic fees are required to maintain the enforceability of the patent. A design patent is generally granted protection for 14 years measured from the date the design patent is granted.", "confidence": 0.32575387402314276, "result_count": 10, "reasons": ["low_confidence"]}
|
| 16 |
+
{"timestamp": "2025-12-05T04:04:13.891362", "query": "Return to work should be phased, however, with three half-days in the first week, two full days in the second week, five half-days in the third week, and full-time by week four. Recovery from knee replacement surgery takes a minimum of three months, but most likely six. A full recovery can take around eight to ten months. The degree of improvement during rehabilitation often depends on the strength of your body before surgery, your body weight, and your ability to manage pain. 1 2 1 1. Many patients are eager to know when they can return to work and resume their normal activities after knee replacement surgery. While the desire for a speedy recovery is nearly universal among patients, less understood and appreciated is the value of recovery time itself.", "confidence": 0.3363066805360089, "result_count": 10, "reasons": ["low_confidence"]}
|
| 17 |
+
{"timestamp": "2025-12-05T04:04:13.896638", "query": "Serratia Marcescens is a human pathogenic species of Serratia. It is sometimes linked to disease in humans. The disease is commonly known as either Serratia plymuthica, Serratia liquefaciens, Serratia rubidaea, Serratia odorifera, or Serratia fonticola. ", "confidence": 0.10608763032101182, "result_count": 10, "reasons": ["low_confidence"]}
|
| 18 |
+
{"timestamp": "2025-12-05T04:04:13.901731", "query": "The viscosity of a fluid is basically a measure of how sticky it is. Water has a fairly low viscosity; things like shampoo or syrup have higher viscosities. Viscosity also depends on temperature: engine oil, for instance, is much less viscous at high temperatures than it is in a cold engine in the middle of winter.", "confidence": 0.22168547234298666, "result_count": 10, "reasons": ["low_confidence"]}
|
| 19 |
+
{"timestamp": "2025-12-05T04:04:13.910398", "query": "Pennsylvania Governor Corbett has ordered all Pennsylvania State flags lowered to half-staff in the Capitol Complex and at Commonwealth facilities in Westmoreland County immediately, October 13, 2011 in honor of Patrolman Derek Kotecki who died in the line of duty on October 12, 2011. Pennsylvania Governor Corbett has ordered all United States flags and Pennsylvania flags lowered to half-staff at the Capitol Complex and at Commonwealth facilities in Northampton County on until sunset on Saturday, September 24, 2011 in honor of Air Force Major Bruce Lawrence who was shot down over Vietnam in 1968.", "confidence": 0.20135046065446768, "result_count": 10, "reasons": ["low_confidence"]}
|
| 20 |
+
{"timestamp": "2025-12-05T04:04:13.915485", "query": "Anatomically and functionally, the esophagus is the least complex section of the digestive tube. Its role in digestion is simple: to convey boluses of food from the pharynx to the stomach. The esophagus begins as an extension of the pharynx in the back of the oral cavity. Absorption in the esophagus is virtually nil. The mucosa does contain mucous glands that are expressed as foodstuffs distend the esophagus, allowing mucus to be secreted and aid in lubrication. The body of the esophagus is bounded by physiologic sphincters known as the upper and lower esophageal sphincters.", "confidence": 0.24093048229383604, "result_count": 10, "reasons": ["low_confidence"]}
|
| 21 |
+
{"timestamp": "2025-12-05T04:04:13.925538", "query": "Research does not support the theory that carbohydrates from wheat, other grains, or starchy vegetables are the source of injury that leads to chronic inflammation. In contrast, scientific research does solidly support that the source of injury leading to chronic inflammation is animal foods. ", "confidence": 0.11461193959930165, "result_count": 10, "reasons": ["low_confidence"]}
|
| 22 |
+
{"timestamp": "2025-12-05T04:04:13.929388", "query": "If you are in there for your 10,000 mile maintenance brake job, than by all means have them resurfaced. A lot of places will even resurface them for free or a very small fee when you buy pads. If it\u2019s a regular maintenance thing, than resurfacing them once or twice is perfectly fine. If you\u2019re just keeping your ship tight with regular brake maintenance, resurfacing is fine and will get you a few extra miles on your rotors. If you\u2019re noticing a problem, sound, or vibration, than you\u2019re dealing with a bigger problem and should replace them. We try to keep it simple. Safety first!", "confidence": 0.14603582187496875, "result_count": 10, "reasons": ["low_confidence"]}
|
| 23 |
+
{"timestamp": "2025-12-05T04:04:13.932928", "query": "Directions. 1 Put the dry seaweed in a large bowl and fill it with cold water. 2 If you like your seaweed crunchy, soak it for 5 minutes, if you like it more tender, soak it for 10 minutes. 3 To make the dressing, combine the rice vinegar, sesame oil, soy sauce, sugar, salt and ginger juice in a small bowl and whisk together. 1 If you like your seaweed crunchy, soak it for 5 minutes, if you like it more tender, soak it for 10 minutes. 2 To make the dressing, combine the rice vinegar, sesame oil, soy sauce, sugar, salt and ginger juice in a small bowl and whisk together. 3 Drain the seaweed and use your hands to squeeze out excess water.", "confidence": 0.18253477190186296, "result_count": 10, "reasons": ["low_confidence"]}
|
| 24 |
+
{"timestamp": "2025-12-05T04:04:13.941938", "query": "Organic herb plant. French Tarragon is a delicately flavored herb reminiscent of mint and licorice that goes particularly well with fish, vinegars, and vegetables. It is delicious in creamy sauces and in combination with chives, garlic, and any lemon-flavored herb. The buttery French sauce, bearnaise, b\u00e9arnaise Includes. tarragon ", "confidence": 0.12814197187040818, "result_count": 10, "reasons": ["low_confidence"]}
|
| 25 |
+
{"timestamp": "2025-12-05T04:04:13.945195", "query": "Q19: What information is available on Where's My Refund? Information on Where's My Refund? is for the most recent tax year we have on file for you. You can check on the status of your refund 24 hours after you e-file. If you filed a paper return, please allow 4 weeks before checking on the status. Even though we issue most refunds in less than 21 days, it\u2019s possible your tax return may require additional review and take longer. Also, if you are anticipating a refund, take into consideration the time it takes for your financial institution to post the refund to your account, or for mail delivery.", "confidence": 0.13153029409341419, "result_count": 10, "reasons": ["low_confidence"]}
|
| 26 |
+
{"timestamp": "2025-12-05T04:04:13.948576", "query": "A conceptual new approach for ligand-receptor capture on the living cell. Ligand receptor capture LRC-TriCEPS\u2122 is a conceptually new technology to analyse the protein interaction of your biologics. We can identify the targets of your protein, antibody and peptide at the cell surface. Ligand-receptor capture technology can be used for many types of orphan ligands, such as peptides, proteins, antibodies, engineered affinity binders but also for viruses. Orphan ligands: 1 Extracellular proteins. 2 Peptide ligands. 3 Antibodies. 4 Engineered affinity binders. 5 Viruses.", "confidence": 0.14592690697904515, "result_count": 10, "reasons": ["low_confidence"]}
|
| 27 |
+
{"timestamp": "2025-12-05T04:04:13.954874", "query": "Regulations, Rules and by-laws are examples of delegated legislation (also called s ubordinate legislation), which is so named because Parliament has delegated power to a local council, government department or other body to make further laws under a particular Act. Acts and Delegated Legislation. Acts (also called statutes) have a name and date, for example the Road Traffic Act 1961 (SA). The name usually reflects the subject matter of the Act and the date indicates the year in which the Act passed through Parliament.", "confidence": 0.21296590264960524, "result_count": 10, "reasons": ["low_confidence"]}
|
| 28 |
+
{"timestamp": "2025-12-05T04:04:13.960836", "query": "ANSWER: It depends. Under Florida's sales and use tax, if no parts were used in the service and the charge was for labor only, then there would be no tax to pay. But if any parts were used in the service work, the store must charge sales tax on the entire bill. If you want further clarification or information on the state's sales tax laws, call the Florida Department of Revenue's toll-free consumer line at 1-800-352-3671, Ext 4.", "confidence": 0.14158310009689912, "result_count": 10, "reasons": ["low_confidence"]}
|
| 29 |
+
{"timestamp": "2025-12-05T04:05:22.847419", "query": "Hydreigon's offensive movepool is enormous; it has access to nearly every single respectable attacking move in the game, which gives it a myriad of options to choose from. On the physical side, Hydreigon has access to Acrobatics, Crunch, Dragon Tail, Outrage, and Head Smash. Overview. Hydreigon belongs to the special group of Pokemon that can boast they possess no true counters: they potentially carry a move that can OHKO or 2HKO any Pokemon in the game, and as such are virtually impossible to switch into. Its peers include such wrecking balls as Deoxys-A, Excadrill, and Salamence.", "confidence": 0.49984776973724365, "result_count": 10, "reasons": ["low_confidence"]}
|
| 30 |
+
{"timestamp": "2025-12-05T04:05:22.850909", "query": "The foremost white Russian variation is the black Russian. A black Russian consists of 3 parts vodka to 2 parts coffee liqueur. For example, measure 3 ounces of vodka and 2 ounces of coffee liqueur. Pour the black Russian over a glass filled with crushed ice.", "confidence": 0.47514423727989197, "result_count": 10, "reasons": ["low_confidence"]}
|
| 31 |
+
{"timestamp": "2025-12-05T04:05:22.864608", "query": "Why it's done. Your doctor may recommend a nuclear stress test to: Diagnose coronary artery disease. Your coronary arteries are the major blood vessels that supply your heart with blood, oxygen and nutrients. If you have symptoms that might indicate coronary artery disease, such as shortness of breath or chest pains, a nuclear stress test can help determine if you have coronary artery disease. 1 See the size and shape of your heart. 2 Guide treatment of heart disorders. 3 Definition. 4 Risks.", "confidence": 0.5250684022903442, "result_count": 10, "reasons": ["low_confidence"]}
|
| 32 |
+
{"timestamp": "2025-12-05T04:05:22.885837", "query": "If you fail to appear for court proceedings regarding a misdemeanor charge, you may be charged with misdemeanor failure to appear. With any misdemeanor charges, you could face up to one year in jail for this charge, along with fines and potentially a suspended license. If you fail to appear for court proceedings related to a felony criminal charge, you will be charged with this additional felony. The punishment for this charge is quite severe at up to one year in state prison and fines up to $5,000.", "confidence": 0.37632015347480774, "result_count": 10, "reasons": ["low_confidence"]}
|
| 33 |
+
{"timestamp": "2025-12-05T04:14:40.082222", "query": "Chris A. Gillespie, MEd, ATC, LAT. THE FACTS Since 2000 exertional sickling is the leading cause of non-traumatic death in NCAA Football \u2026 All Divisions. In FBS --- if you add heat, heart, and asthma --- Combined, match the total dead from sickling. ", "confidence": 0.5787151512765106, "result_count": 10, "reasons": ["low_confidence"]}
|
| 34 |
+
{"timestamp": "2025-12-05T04:14:40.405485", "query": "Of all the snakes on this list, the ball python sits right at the edge of a good beginner snake. It has more specific care requirements than the others. In addition to its care requirements, the ball python often stops eating, or going \u201coff feed\u201d for whatever reason at any time of the year. We often get questions about what is an ideal beginner-friendly snake for those new to the hobby. Beginner meaning fairly easy to care for with not a lot of requirements other than good husbandry and attention to detail. Of all the reptiles available in the hobby, snakes seem to be the most popular. Go to any reptile show, and the majority of the animals available are of the legless kind", "confidence": 0.4973230852988249, "result_count": 10, "reasons": ["low_confidence"]}
|
| 35 |
+
{"timestamp": "2025-12-05T04:14:40.433679", "query": "Vesphene IIse Germicidal Detergent is intended for use in institutions such as hospitals, nursing homes, schools, medical and dental offices, pharmaceutical plants, and other indoor areas where disinfection, cleaning, and deodorizing are necessary. For full access to this content, please Register or Sign In. Vesphene IIse Germicidal Detergent cleans and disinfects all washable hard non-porous environmental surfaces such as floors, walls, woodwork, bathroom fixtures, equipment, and furniture.", "confidence": 0.46005434348332924, "result_count": 10, "reasons": ["low_confidence"]}
|
| 36 |
+
{"timestamp": "2025-12-05T04:14:40.517460", "query": "ANSWER: It depends. Under Florida's sales and use tax, if no parts were used in the service and the charge was for labor only, then there would be no tax to pay. But if any parts were used in the service work, the store must charge sales tax on the entire bill. If you want further clarification or information on the state's sales tax laws, call the Florida Department of Revenue's toll-free consumer line at 1-800-352-3671, Ext 4.", "confidence": 0.505778632955981, "result_count": 10, "reasons": ["low_confidence"]}
|
| 37 |
+
{"timestamp": "2025-12-05T04:23:23.629043", "query": "1 Caring for one pet should cost less than caring for multiple pets. 2 If you have a number of animals, expect to pay more for services. 3 The type of pet you have is a determining factor in how much the sitter may charge. 4 Pet sitters usually charge less for cats and hamsters, for example, than they do for dogs. 1 A 30-minute walk may go for $10 or $15 dollars. 2 If you require the pet sitter to spend the night with your sick or lonely pet, expect to pay extra. 3 The usual charge is from $40 to $60 a night.", "confidence": 0.11957169270954886, "result_count": 10, "reasons": ["low_confidence"]}
|
| 38 |
+
{"timestamp": "2025-12-05T04:23:23.642537", "query": "You\u2019ll also need to pay for the CDL itself; CDL costs range from $25 to $100. Special endorsements such as air brakes, doubles/triples, and hazardous materials may cost between $5 and $45 each. You may not have to pay the cost of truck driving school on your own, though. The average cost of PTDI-certified courses is about $4,200, though the cost of truck driving school may be as low as $1,500 and as high as $10,000. Related program expenses that may or may not be included in truck driving school tuition are books, uniforms, and fees for tests, medical exams, and graduation.", "confidence": 0.18618601375859162, "result_count": 10, "reasons": ["low_confidence"]}
|
| 39 |
+
{"timestamp": "2025-12-05T04:23:23.655558", "query": "LOCATION AND SIZE. Iraq is located in the Middle East, between Iran and Saudi Arabia. Iraq is also bordered by Jordan and Syria to the west, Kuwait to the south, and Turkey to the north. A very small sliver of the Persian Gulf (58 kilometers, or 36.04 miles) abuts Iraq on its southeast border. With an area of 437,072 square kilometers (168,753 square miles), Iraq is slightly more than twice the size of Idaho. Iraq's capital city, Baghdad, is located in the center of the country. Other major cities include al-Basra in the south and Mosul in the north", "confidence": 0.34377298418659913, "result_count": 10, "reasons": ["low_confidence"]}
|
| 40 |
+
{"timestamp": "2025-12-05T04:23:23.672735", "query": "Glandular fever is a viral infection caused by the Epstein\u2013Barr virus. Glandular fever is often spread through oral acts such as kissing, which is why it is sometimes called the kissing disease. However, glandular fever can also be spread by airborne saliva droplets. Symptoms of glandular fever include: 1 Fever. ", "confidence": 0.40762822012572364, "result_count": 10, "reasons": ["low_confidence"]}
|
| 41 |
+
{"timestamp": "2025-12-05T04:23:23.678416", "query": "One thing I like about Vistex Support is the fact that when you have an issue with it you can log a message through the service marketplace from SAP itself (service.sap.com), and the message is routed to a vistex development directly, which sure speeds things up. Good Luck, SAP Logistics Sales and Distribution. The SAP Logistics SD group is for the discussion of specific configuration, administration, and development issues that arise when utilizing the SAP Logistics Sales and Distribution component.", "confidence": 0.2921026160124872, "result_count": 10, "reasons": ["low_confidence"]}
|
| 42 |
+
{"timestamp": "2025-12-05T04:23:23.683881", "query": "Flavonoids are a group of plant metabolites thought to provide health benefits through cell signalling pathways and antioxidant effects. These molecules are found in a variety of fruits and vegetables. Flavonoids are polyphenolic molecules containing 15 carbon atoms and are soluble in water. The abundance of flavonoids coupled with their low toxicity relative to other plant compounds means they can be ingested in large quantities by animals, including humans. Examples of foods that are rich in flavonoids include onions, parsley, blueberries, bananas, dark chocolate and red wine.", "confidence": 0.34804435928642086, "result_count": 10, "reasons": ["low_confidence"]}
|
| 43 |
+
{"timestamp": "2025-12-05T04:23:23.689161", "query": "Glucose, or simple sugar, is a solid until it reaches a temperature of 145 to 150 Co. At that point, it melts, it becomes a liquid. Sugar will not boil before the applied heat will begin to pyrolyze or, in the presence of air (oxygen) burn. ", "confidence": 0.17559626322839966, "result_count": 10, "reasons": ["low_confidence"]}
|
| 44 |
+
{"timestamp": "2025-12-05T04:23:23.695286", "query": "Discuss the Role of the Early Years Practitioner in Planning Provision to Meet the Needs of the Child. Text Preview. Discuss the role of the early years practitioner in planning provision to meet the needs of the child. This essay aims to explore the role of the early years practitioner in planning provision to meet the needs of the child, simultaneously applying theoretical research and professional practice.", "confidence": 0.09886203232531417, "result_count": 10, "reasons": ["low_confidence"]}
|
| 45 |
+
{"timestamp": "2025-12-05T04:23:23.700640", "query": "From a low down payment mortgage to using your Registered Retirement Savings Plan (RRSP) as a source of funds, buying a home has never been easier. The down payment is that portion of the purchase price you furnish yourself. The balance is obtained from a financial institution in the form of a mortgage. The withdrawal is not taxable as long as you repay it within a 15-year period. To qualify, the RRSP funds you plan to use must have been in your RRSP for at least 90 days. Even if you already have enough money for your down payment, it may make sense to access your RRSP savings through the Home Buyers' Plan.", "confidence": 0.4254641849725119, "result_count": 10, "reasons": ["low_confidence"]}
|
| 46 |
+
{"timestamp": "2025-12-05T04:23:23.710947", "query": "Gastrointestinal Effects. Brewer's yeast is sometimes used to treat diarrhea and constipation. It can have a similar water-binding effect to fiber. The more common side effects of supplementing with brewer's yeast are those of a gastrointestinal nature, such as gas, flatulence and a laxative effect. Brewer's yeast can have adverse effects as well as beneficial ones. You can use brewer's yeast, the type used to make beer, not bread, as a nutritional supplement. It can potentially lower your risk for high cholesterol and help you control your weight and blood sugar levels, according to the University of Maryland Medical Center website", "confidence": 0.2242658315328104, "result_count": 10, "reasons": ["low_confidence"]}
|
| 47 |
+
{"timestamp": "2025-12-05T04:23:23.716510", "query": "Elevation Effects. For every 1,000 feet of change in elevation, there is a loss of 1 foot in suction or lift and a 0.5 pounds per square inch decrease in atmospheric pressure. Example 2 - An engine can lift water 22.5 feet at sea level. The same engine is driven to a fire at an elevation of 2,000 feet above", "confidence": 0.13683717029393855, "result_count": 10, "reasons": ["low_confidence"]}
|
| 48 |
+
{"timestamp": "2025-12-05T04:23:23.722165", "query": "Cefuroxime is used to treat certain infections caused by bacteria, such as bronchitis; gonorrhea; Lyme disease; and infections of the ears, throat, sinuses, urinary tract, and skin. Cefuroxime is in a class of medications called cephalosporin antibiotics. It works by stopping the growth of bacteria.", "confidence": 0.2563230222960035, "result_count": 10, "reasons": ["low_confidence"]}
|
| 49 |
+
{"timestamp": "2025-12-05T04:23:23.727727", "query": "HRMS also refers to the integration of human resource management and information technology to automate and facilitate human resource activities. The general notion of an HRMS helps small-business managers craft suitable human resource systems based on their field of business and business growth stage. In a broad definition, a human resource management system, or HRMS, encompasses the highest level of human resource management activities. It is a program of multiple human resource policies that are internally consistent in relation to a human resource objective.", "confidence": 0.14126607329632876, "result_count": 10, "reasons": ["low_confidence"]}
|
| 50 |
+
{"timestamp": "2025-12-05T04:23:23.735148", "query": "Kombucha is a fermented tea which contains lots of probiotics to improve your health. If you're hesitant to try organic kombucha tea because it's fermented and has a unique smell, take a chance and it may soon become your favorite beverage. Kombucha is an ancient recipe for traditional tea that originated in China. Organic ingredients, like the tea and sugar in organic kombucha, come from farms that do not use synthetic fertilizers or pesticides or genetically engineered foods. It also means that the farm and food have been scrutinized to meet the USDA requirements.", "confidence": 0.2486887518366076, "result_count": 10, "reasons": ["low_confidence"]}
|
| 51 |
+
{"timestamp": "2025-12-05T04:23:23.744467", "query": "Fertile chicken eggs can be stored up to 10 days (before incubating) with little loss in hatchability \u2013 as long as you keep them out of the refrigerator. The ideal storage conditions are 55 to 60 degrees Fahrenheit and 70 to 75 percent relative humidity. The easiest way to incubate and hatch fertile chicken eggs is to have a broody hen do all the work for you. What\u2019s a broody hen, you wonder? This hen has undergone progesterone-induced changes that make her want to sit on eggs to hatch them and brood the resulting chicks.", "confidence": 0.12317020216125468, "result_count": 10, "reasons": ["low_confidence"]}
|
| 52 |
+
{"timestamp": "2025-12-05T04:23:23.751434", "query": "I heard before that they were, but then I\u2019m a sushi cheif, and I know that they are used to set food on and as decoration, and some asian coutries stem sweet rice inside of them. No i dont think so. Yes you are right, cheif frequently use bamboo leaves to wrape and also as an ingredient of recepie. Bamboo shoots are very common in Thai cousine. Bamboo is a woody grass\u2014not a tree. There are approximately 1,000 species of bamboo from small plants to giant timber bamboos that can grow to over 40m. Bamboo grows in temperate and tropical countries around the world, but is best known in China and S.E. Asia. It is used for food, construction, and making tools. ", "confidence": 0.136365011466467, "result_count": 10, "reasons": ["low_confidence"]}
|
| 53 |
+
{"timestamp": "2025-12-05T04:23:23.757415", "query": "Ayurveda (Sanskrit: \u0906\u092f\u0941\u0930\u094d\u0935\u0947\u0926 \u0100yurveda, life-knowledge ; English pronunciation /\u02cca\u026a.\u0259r\u02c8ve\u026ad\u0259/) or Ayurvedic medicine is a system of medicine with historical roots in the Indian subcontinent. The use of opium is not found in the ancient Ayurvedic texts, and is first mentioned in the Sarngadhara Samhita (1300-1400 CE), a book on pharmacy used in Rajasthan in Western India, as an ingredient of an aphrodisiac to delay male ejaculation.", "confidence": 0.13617304541428732, "result_count": 10, "reasons": ["low_confidence"]}
|
| 54 |
+
{"timestamp": "2025-12-05T04:23:23.769459", "query": "Yohimbe contains a chemical that affects the body. This chemical is called yohimbine. Yohimbine might affect the body in some of the same ways as some medications for depression called MAOIs. Taking yohimbe along with MAOIs might increase the effects and side effects of yohimbe and MAOIs. Yohimbe contains a chemical that can affect the brain. This chemical is called yohimbine. Naloxone (Narcan) also affects the brain. Taking naloxone (Narcan) along with yohimbine might increase the chance of side effects such as anxiety, nervousness, trembling, and hot flashes.", "confidence": 0.2631052261136738, "result_count": 10, "reasons": ["low_confidence"]}
|
| 55 |
+
{"timestamp": "2025-12-05T04:33:20.049816", "query": "The Cannondale Bicycle Corporation, is an American division of Canadian conglomerate Dorel Industries that supplies bicycles. It is headquartered in Wilton, Connecticut with manufacturing and assembly facilities in China and Taichung, Taiwan. The Lefty is now seen on many of Cannondale's high-end models, such as all the Scalpels, Rizes, and the expensive models in F series, both cross-country lines. Continual efforts at weight reduction have provided models with a carbon fiber upper tube and a titanium spindle.", "confidence": 0.5676314830780029, "result_count": 10, "reasons": ["low_confidence"]}
|
| 56 |
+
{"timestamp": "2025-12-05T04:33:20.169625", "query": "Biomarkers (short for biological markers) are biological measures of a biological state. By definition, a biomarker is a characteristic that is objectively measured and evaluated as an indicator of normal biological processes, pathogenic processes or pharmacological responses to a therapeutic intervention..", "confidence": 0.37469613552093506, "result_count": 10, "reasons": ["low_confidence"]}
|
| 57 |
+
{"timestamp": "2025-12-05T04:33:20.188712", "query": "Visible light has frequencies ranging from 4*10 14 Hz to 8*10 14 Hz (400 THz to 800 THz) and wavelengths from 3.8*10 -7 m to 7.5*10 -7 m (380 nm to 750 nm). Red light has the lowest frequency (longest wavelength) and violet light has the highest frequency (shortest wavelength) of visible light.", "confidence": 0.3559046983718872, "result_count": 10, "reasons": ["low_confidence"]}
|
| 58 |
+
{"timestamp": "2025-12-05T04:33:20.344797", "query": "For example, let's assume it costs Company XYZ $10,000 to purchase 5,000 widgets that it will resell in its retail outlets. Company XYZ's cost per unit is: $10,000 / 5,000 = $2 per unit. Often, calculating the cost per unit isn't so simple, especially in manufacturing situations. Usually, costs per unit involve variable costs (costs that vary with the number of units made) and fixed costs (costs that don't vary with the number of units made). For example, at XYZ Restaurant, which sells only pepperoni pizza, the variable expenses per pizza might be: Flour: $0.50. Yeast: $0.05. ", "confidence": 0.5727376937866211, "result_count": 10, "reasons": ["low_confidence"]}
|
| 59 |
+
{"timestamp": "2025-12-05T04:33:20.367353", "query": "A bridal or wedding set is the engagement ring and wedding ring for the woman sold as 1 together. You can pull them apart and they are made to clip together. And they match! Buy the bridal set and give the engagement ring to the woman and then tell her you already have the matching wedding ring for it. An engagement ring is a ring given to propose (1 ring by itself. This is the BLING ring. The one with a big diamond 1-2 cts)", "confidence": 0.5388101935386658, "result_count": 10, "reasons": ["low_confidence"]}
|
main.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from tabulate import tabulate
|
| 5 |
+
import itertools
|
| 6 |
+
|
| 7 |
+
from config import (
|
| 8 |
+
NUM_CLUSTERS, FRESHNESS_SHARD_ID, MRL_DIMS,
|
| 9 |
+
EMBEDDING_MODELS, ROUTER_MODELS, COLLECTION_NAME
|
| 10 |
+
)
|
| 11 |
+
from src.data_pipeline import get_embeddings, mrl_slice, load_ms_marco, generate_synthetic_data
|
| 12 |
+
from src.router import LearnedRouter
|
| 13 |
+
from src.vector_db import UnifiedQdrant
|
| 14 |
+
from src.active_learning import log_for_retraining
|
| 15 |
+
|
| 16 |
+
def run_benchmark():
|
| 17 |
+
print("============================================================")
|
| 18 |
+
print(" xVector / dashVector: Learned Hybrid Retrieval Engine ")
|
| 19 |
+
print("============================================================")
|
| 20 |
+
|
| 21 |
+
results_table = []
|
| 22 |
+
|
| 23 |
+
# P&C Matrix: Iterate through all Embedding Models x Router Models
|
| 24 |
+
combinations = list(itertools.product(EMBEDDING_MODELS.keys(), ROUTER_MODELS))
|
| 25 |
+
|
| 26 |
+
for embed_name, router_name in combinations:
|
| 27 |
+
print(f"\n>>> Running Experiment: Embedding='{embed_name}' | Router='{router_name}'")
|
| 28 |
+
|
| 29 |
+
model_id = EMBEDDING_MODELS[embed_name]
|
| 30 |
+
|
| 31 |
+
# 1. Generate/Load Data
|
| 32 |
+
# We need enough data to cluster meaningfully.
|
| 33 |
+
N_SAMPLES = 2000
|
| 34 |
+
raw_texts = load_ms_marco(N_SAMPLES)
|
| 35 |
+
|
| 36 |
+
# Generate Embeddings
|
| 37 |
+
embeddings = get_embeddings(model_id, raw_texts)
|
| 38 |
+
vector_dim = embeddings.shape[1]
|
| 39 |
+
|
| 40 |
+
# Split into Train (for Router) and Index (for DB)
|
| 41 |
+
# In a real scenario, we might train on a subset and index everything.
|
| 42 |
+
# Here, let's use 50% for training router, and index the other 50% + some "fresh" data.
|
| 43 |
+
split_idx = int(N_SAMPLES * 0.5)
|
| 44 |
+
X_train = embeddings[:split_idx]
|
| 45 |
+
X_index = embeddings[split_idx:]
|
| 46 |
+
texts_index = raw_texts[split_idx:]
|
| 47 |
+
|
| 48 |
+
# 2. Train Router
|
| 49 |
+
router = LearnedRouter(model_type=router_name, n_clusters=NUM_CLUSTERS, mrl_dims=MRL_DIMS)
|
| 50 |
+
router.train(X_train)
|
| 51 |
+
|
| 52 |
+
# 3. Index Data
|
| 53 |
+
# We need to assign clusters to X_index using the router (or ground truth?)
|
| 54 |
+
# For the "Index Data" phase, we usually index based on the Router's prediction
|
| 55 |
+
# OR we can index based on Ground Truth K-Means if we want the DB to be perfect,
|
| 56 |
+
# and then test if the Router can find it.
|
| 57 |
+
# The prompt says: "The Brain (Router)... predicts which data partition... contains the answer".
|
| 58 |
+
# Usually, we partition data using K-Means (Ground Truth) during ingestion.
|
| 59 |
+
# Then at query time, the Router predicts where to look.
|
| 60 |
+
|
| 61 |
+
# So:
|
| 62 |
+
# A. Run K-Means on X_index to determine where they SHOULD go.
|
| 63 |
+
# (Ideally, we use the SAME K-Means model from training if possible, but K-Means is transductive.
|
| 64 |
+
# We should probably use the router's kmeans to predict labels for X_index)
|
| 65 |
+
|
| 66 |
+
# Let's use the router's internal kmeans to assign ground truth labels for indexing.
|
| 67 |
+
# This ensures consistency.
|
| 68 |
+
ground_truth_labels = router.kmeans.predict(X_index)
|
| 69 |
+
|
| 70 |
+
# Initialize DB
|
| 71 |
+
db = UnifiedQdrant(
|
| 72 |
+
collection_name=COLLECTION_NAME,
|
| 73 |
+
vector_size=vector_dim,
|
| 74 |
+
num_clusters=NUM_CLUSTERS,
|
| 75 |
+
freshness_shard_id=FRESHNESS_SHARD_ID
|
| 76 |
+
)
|
| 77 |
+
db.initialize()
|
| 78 |
+
|
| 79 |
+
# Prepare payloads
|
| 80 |
+
payloads = [{"text": t, "origin": "historical"} for t in texts_index]
|
| 81 |
+
|
| 82 |
+
# Index Historical Data (Assigned to specific clusters)
|
| 83 |
+
db.index_data(X_index, payloads, ground_truth_labels)
|
| 84 |
+
|
| 85 |
+
# Index some "Fresh" Data (No cluster assigned -> Freshness Shard)
|
| 86 |
+
# Let's simulate 100 fresh items
|
| 87 |
+
fresh_texts = generate_synthetic_data(100)
|
| 88 |
+
fresh_embeddings = get_embeddings(model_id, fresh_texts)
|
| 89 |
+
fresh_payloads = [{"text": t, "origin": "fresh"} for t in fresh_texts]
|
| 90 |
+
db.index_data(fresh_embeddings, fresh_payloads, [None] * len(fresh_texts))
|
| 91 |
+
|
| 92 |
+
# 4. Run Test Queries
|
| 93 |
+
# We'll use a subset of X_index as queries to see if we can find them back (Self-Recall)
|
| 94 |
+
# And maybe some completely new queries.
|
| 95 |
+
test_indices = np.random.choice(len(X_index), size=20, replace=False)
|
| 96 |
+
test_queries = X_index[test_indices]
|
| 97 |
+
test_query_texts = [texts_index[i] for i in test_indices]
|
| 98 |
+
|
| 99 |
+
latencies = []
|
| 100 |
+
hits = 0
|
| 101 |
+
shards_searched_count = 0
|
| 102 |
+
|
| 103 |
+
print(" - Running Test Queries...")
|
| 104 |
+
for i, query_vec in enumerate(test_queries):
|
| 105 |
+
start_time = time.time()
|
| 106 |
+
|
| 107 |
+
# Router Prediction
|
| 108 |
+
target_cluster, confidence = router.predict(query_vec)
|
| 109 |
+
|
| 110 |
+
# Search
|
| 111 |
+
results, search_mode = db.search_hybrid(query_vec, target_cluster, confidence)
|
| 112 |
+
|
| 113 |
+
end_time = time.time()
|
| 114 |
+
latencies.append((end_time - start_time) * 1000) # ms
|
| 115 |
+
|
| 116 |
+
# Check if we found the correct document (Self-Recall)
|
| 117 |
+
# We look for the text in the results
|
| 118 |
+
target_text = test_query_texts[i]
|
| 119 |
+
found = any(res.payload['text'] == target_text for res in results)
|
| 120 |
+
if found:
|
| 121 |
+
hits += 1
|
| 122 |
+
|
| 123 |
+
# Log for Active Learning
|
| 124 |
+
log_for_retraining(target_text, confidence, results)
|
| 125 |
+
|
| 126 |
+
# Track efficiency
|
| 127 |
+
if "GLOBAL" in search_mode:
|
| 128 |
+
shards_searched_count += (NUM_CLUSTERS + 1)
|
| 129 |
+
else:
|
| 130 |
+
shards_searched_count += 2 # Target + Freshness
|
| 131 |
+
|
| 132 |
+
# 5. Metrics
|
| 133 |
+
avg_latency = np.mean(latencies)
|
| 134 |
+
accuracy = hits / len(test_queries)
|
| 135 |
+
avg_shards = shards_searched_count / len(test_queries)
|
| 136 |
+
total_shards = NUM_CLUSTERS + 1
|
| 137 |
+
savings = (1 - (avg_shards / total_shards)) * 100
|
| 138 |
+
|
| 139 |
+
results_table.append([
|
| 140 |
+
embed_name, router_name,
|
| 141 |
+
f"{accuracy:.2%}", f"{avg_latency:.2f} ms",
|
| 142 |
+
f"{savings:.1f}%"
|
| 143 |
+
])
|
| 144 |
+
|
| 145 |
+
# Print Summary
|
| 146 |
+
print("\n\n================ RESULTS SUMMARY ================")
|
| 147 |
+
headers = ["Embedding", "Router", "Accuracy", "Latency", "Compute Savings"]
|
| 148 |
+
print(tabulate(results_table, headers=headers, tablefmt="grid"))
|
| 149 |
+
|
| 150 |
+
if __name__ == "__main__":
|
| 151 |
+
run_benchmark()
|
notebooks/xVector_Analysis.ipynb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# xVector Analysis\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"This notebook is a template for visualizing the results of the dashVector / xVector engine.\n",
|
| 10 |
+
"It connects to the generated logs and the Qdrant instance to provide insights."
|
| 11 |
+
]
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"cell_type": "code",
|
| 15 |
+
"execution_count": null,
|
| 16 |
+
"metadata": {},
|
| 17 |
+
"outputs": [],
|
| 18 |
+
"source": [
|
| 19 |
+
"import pandas as pd\n",
|
| 20 |
+
"import matplotlib.pyplot as plt\n",
|
| 21 |
+
"import json\n",
|
| 22 |
+
"import os\n",
|
| 23 |
+
"\n",
|
| 24 |
+
"# Path to logs\n",
|
| 25 |
+
"LOG_FILE = \"../logs/active_learning_queue.jsonl\"\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"def load_logs():\n",
|
| 28 |
+
" data = []\n",
|
| 29 |
+
" if os.path.exists(LOG_FILE):\n",
|
| 30 |
+
" with open(LOG_FILE, 'r') as f:\n",
|
| 31 |
+
" for line in f:\n",
|
| 32 |
+
" data.append(json.loads(line))\n",
|
| 33 |
+
" return pd.DataFrame(data)\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"df = load_logs()\n",
|
| 36 |
+
"if not df.empty:\n",
|
| 37 |
+
" print(f\"Loaded {len(df)} log entries.\")\n",
|
| 38 |
+
" display(df.head())\n",
|
| 39 |
+
"else:\n",
|
| 40 |
+
" print(\"No logs found yet. Run main.py first.\")"
|
| 41 |
+
]
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"cell_type": "markdown",
|
| 45 |
+
"metadata": {},
|
| 46 |
+
"source": [
|
| 47 |
+
"## Confidence Distribution\n",
|
| 48 |
+
"Analyze the confidence scores of queries that triggered active learning."
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"cell_type": "code",
|
| 53 |
+
"execution_count": null,
|
| 54 |
+
"metadata": {},
|
| 55 |
+
"outputs": [],
|
| 56 |
+
"source": [
|
| 57 |
+
"if not df.empty:\n",
|
| 58 |
+
" plt.figure(figsize=(10, 6))\n",
|
| 59 |
+
" plt.hist(df['confidence'], bins=20, color='skyblue', edgecolor='black')\n",
|
| 60 |
+
" plt.title('Distribution of Confidence Scores (Hard Negatives)')\n",
|
| 61 |
+
" plt.xlabel('Confidence')\n",
|
| 62 |
+
" plt.ylabel('Count')\n",
|
| 63 |
+
" plt.show()"
|
| 64 |
+
]
|
| 65 |
+
}
|
| 66 |
+
],
|
| 67 |
+
"metadata": {
|
| 68 |
+
"kernelspec": {
|
| 69 |
+
"display_name": "Python 3",
|
| 70 |
+
"language": "python",
|
| 71 |
+
"name": "python3"
|
| 72 |
+
},
|
| 73 |
+
"language_info": {
|
| 74 |
+
"codemirror_mode": {
|
| 75 |
+
"name": "ipython",
|
| 76 |
+
"version": 3
|
| 77 |
+
},
|
| 78 |
+
"file_extension": ".py",
|
| 79 |
+
"mimetype": "text/x-python",
|
| 80 |
+
"name": "python",
|
| 81 |
+
"nbconvert_exporter": "python",
|
| 82 |
+
"pygments_lexer": "ipython3",
|
| 83 |
+
"version": "3.8.10"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"nbformat": 4,
|
| 87 |
+
"nbformat_minor": 5
|
| 88 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
qdrant-client>=1.10.0
|
| 2 |
+
sentence-transformers
|
| 3 |
+
lightgbm
|
| 4 |
+
scikit-learn
|
| 5 |
+
datasets
|
| 6 |
+
numpy
|
| 7 |
+
pandas
|
| 8 |
+
tqdm
|
| 9 |
+
einops
|
| 10 |
+
gradio
|
scripts/ingest_ms_marco.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import numpy as np
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
|
| 6 |
+
# Add project root to path
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 8 |
+
|
| 9 |
+
from config import (
|
| 10 |
+
NUM_CLUSTERS, FRESHNESS_SHARD_ID, MRL_DIMS,
|
| 11 |
+
EMBEDDING_MODELS, ROUTER_MODELS, COLLECTION_NAME,
|
| 12 |
+
QDRANT_URL, QDRANT_API_KEY
|
| 13 |
+
)
|
| 14 |
+
from src.data_pipeline import get_embeddings, load_ms_marco
|
| 15 |
+
from src.router import LearnedRouter
|
| 16 |
+
from src.vector_db import UnifiedQdrant
|
| 17 |
+
|
| 18 |
+
def ingest_data():
|
| 19 |
+
print(">>> Starting Ingestion Pipeline for Qdrant Cloud...")
|
| 20 |
+
|
| 21 |
+
if QDRANT_URL == ":memory:":
|
| 22 |
+
print("WARNING: QDRANT_URL is still :memory:. Please set QDRANT_URL env var for production.")
|
| 23 |
+
# We continue anyway for testing logic, but warn user.
|
| 24 |
+
|
| 25 |
+
# 1. Load Data (101k samples for production proof)
|
| 26 |
+
# For demo speed, we might start with 10k, but let's aim for 20k to be significant.
|
| 27 |
+
N_SAMPLES = 1000
|
| 28 |
+
print(f"Loading {N_SAMPLES} samples from MS MARCO...")
|
| 29 |
+
raw_texts = load_ms_marco(N_SAMPLES)
|
| 30 |
+
|
| 31 |
+
# 2. Generate Embeddings
|
| 32 |
+
# Use 'nomic' or 'minilm'. Let's stick to 'minilm' for speed/reliability in this demo unless specified.
|
| 33 |
+
# Config says 'nomic' is primary, but 'minilm' is baseline.
|
| 34 |
+
# Let's use 'minilm' for the first pass to ensure it works, or 'nomic' if we want MRL power.
|
| 35 |
+
# The prompt mentioned MRL optimization, so 'nomic' is better if we want real MRL.
|
| 36 |
+
# However, 'minilm' is 384 dims. 'nomic' is 768.
|
| 37 |
+
# Our config MRL_DIMS is 64.
|
| 38 |
+
# Let's use 'minilm' as it's faster to download/run on CPU if needed.
|
| 39 |
+
MODEL_NAME = EMBEDDING_MODELS["minilm"]
|
| 40 |
+
print(f"Generating embeddings using {MODEL_NAME}...")
|
| 41 |
+
embeddings = get_embeddings(MODEL_NAME, raw_texts)
|
| 42 |
+
vector_dim = embeddings.shape[1]
|
| 43 |
+
|
| 44 |
+
# 3. Train Router
|
| 45 |
+
# We need to train the router on this data to cluster it.
|
| 46 |
+
print("Training Router...")
|
| 47 |
+
router = LearnedRouter(model_type="lightgbm", n_clusters=NUM_CLUSTERS, mrl_dims=MRL_DIMS)
|
| 48 |
+
router.train(embeddings)
|
| 49 |
+
|
| 50 |
+
# Save Router
|
| 51 |
+
os.makedirs("models", exist_ok=True)
|
| 52 |
+
router.save("models/router_v1.pkl")
|
| 53 |
+
|
| 54 |
+
# 4. Assign Clusters (Ground Truth for Indexing)
|
| 55 |
+
print("Assigning clusters...")
|
| 56 |
+
# We use the router's internal KMeans to get the "Ground Truth" cluster for each point.
|
| 57 |
+
# This ensures that the data actually lives where the router *should* predict it to be (mostly).
|
| 58 |
+
cluster_ids = router.kmeans.predict(embeddings)
|
| 59 |
+
|
| 60 |
+
# 5. Index to Qdrant
|
| 61 |
+
print("Initializing Qdrant...")
|
| 62 |
+
db = UnifiedQdrant(
|
| 63 |
+
collection_name=COLLECTION_NAME,
|
| 64 |
+
vector_size=vector_dim,
|
| 65 |
+
num_clusters=NUM_CLUSTERS,
|
| 66 |
+
freshness_shard_id=FRESHNESS_SHARD_ID
|
| 67 |
+
)
|
| 68 |
+
db.initialize()
|
| 69 |
+
|
| 70 |
+
print("Indexing data...")
|
| 71 |
+
# Batching is handled inside index_data somewhat, but let's pass it all
|
| 72 |
+
# The index_data method groups by shard, which is efficient for custom sharding.
|
| 73 |
+
|
| 74 |
+
payloads = [{"text": t, "origin": "ms_marco"} for t in raw_texts]
|
| 75 |
+
|
| 76 |
+
# We can process in chunks to avoid OOM if 20k is too big for memory (it's fine for 20k).
|
| 77 |
+
db.index_data(embeddings, payloads, cluster_ids)
|
| 78 |
+
|
| 79 |
+
print(">>> Ingestion Complete!")
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
ingest_data()
|
src/__init__.py
ADDED
|
File without changes
|
src/active_learning.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import List, Any
|
| 5 |
+
from config import ACTIVE_LEARNING_LOG
|
| 6 |
+
|
| 7 |
+
def log_for_retraining(query: str, confidence: float, results: List[Any]):
|
| 8 |
+
"""
|
| 9 |
+
Logs queries that have low confidence or zero results for active learning (retraining).
|
| 10 |
+
|
| 11 |
+
Logic:
|
| 12 |
+
- If confidence < 0.6 OR len(results) == 0:
|
| 13 |
+
Append to logs/active_learning_queue.jsonl
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
should_log = False
|
| 17 |
+
reason = []
|
| 18 |
+
|
| 19 |
+
if confidence < 0.6:
|
| 20 |
+
should_log = True
|
| 21 |
+
reason.append("low_confidence")
|
| 22 |
+
|
| 23 |
+
if len(results) == 0:
|
| 24 |
+
should_log = True
|
| 25 |
+
reason.append("zero_results")
|
| 26 |
+
|
| 27 |
+
if should_log:
|
| 28 |
+
entry = {
|
| 29 |
+
"timestamp": datetime.now().isoformat(),
|
| 30 |
+
"query": query,
|
| 31 |
+
"confidence": float(confidence),
|
| 32 |
+
"result_count": len(results),
|
| 33 |
+
"reasons": reason
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
with open(ACTIVE_LEARNING_LOG, "a") as f:
|
| 38 |
+
f.write(json.dumps(entry) + "\n")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Error logging for active learning: {e}")
|
src/comparison.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import List, Dict, Any, Tuple
|
| 4 |
+
from src.vector_db import UnifiedQdrant
|
| 5 |
+
from src.router import LearnedRouter
|
| 6 |
+
from src.data_pipeline import get_embeddings
|
| 7 |
+
from config import NUM_CLUSTERS, FRESHNESS_SHARD_ID, EMBEDDING_MODELS
|
| 8 |
+
|
| 9 |
+
class ComparisonEngine:
|
| 10 |
+
def __init__(self, db: UnifiedQdrant, router: LearnedRouter, embedding_model_name: str = "minilm"):
|
| 11 |
+
self.db = db
|
| 12 |
+
self.router = router
|
| 13 |
+
self.embedding_model_name = EMBEDDING_MODELS.get(embedding_model_name, embedding_model_name)
|
| 14 |
+
|
| 15 |
+
def get_query_embedding(self, query: str) -> np.ndarray:
|
| 16 |
+
# Returns 1D array
|
| 17 |
+
emb = get_embeddings(self.embedding_model_name, [query])
|
| 18 |
+
return emb[0]
|
| 19 |
+
|
| 20 |
+
def direct_search(self, query: str) -> Dict[str, Any]:
|
| 21 |
+
"""
|
| 22 |
+
Brute Force Search (Baseline).
|
| 23 |
+
Searches ALL shards.
|
| 24 |
+
"""
|
| 25 |
+
query_vec = self.get_query_embedding(query)
|
| 26 |
+
|
| 27 |
+
start_time = time.time()
|
| 28 |
+
|
| 29 |
+
# In Qdrant, searching without shard_key_selector searches all shards.
|
| 30 |
+
# However, our UnifiedQdrant.search_hybrid is designed for hybrid.
|
| 31 |
+
# We need a raw search method or just use the client directly.
|
| 32 |
+
# Let's use the client directly to be pure "Brute Force".
|
| 33 |
+
|
| 34 |
+
# Note: In local mode, everything is one collection anyway.
|
| 35 |
+
# In Cloud with custom sharding, omitting shard_key searches all.
|
| 36 |
+
|
| 37 |
+
if self.db.is_local:
|
| 38 |
+
results = self.db.client.query_points(
|
| 39 |
+
collection_name=self.db.collection_name,
|
| 40 |
+
query=query_vec,
|
| 41 |
+
limit=10
|
| 42 |
+
).points
|
| 43 |
+
else:
|
| 44 |
+
results = self.db.client.query_points(
|
| 45 |
+
collection_name=self.db.collection_name,
|
| 46 |
+
query=query_vec,
|
| 47 |
+
limit=10
|
| 48 |
+
# No shard_key_selector -> Global Search
|
| 49 |
+
).points
|
| 50 |
+
|
| 51 |
+
end_time = time.time()
|
| 52 |
+
latency_ms = (end_time - start_time) * 1000
|
| 53 |
+
|
| 54 |
+
# Compute Units: All Clusters + Freshness
|
| 55 |
+
shards_searched = self.db.num_clusters + 1
|
| 56 |
+
|
| 57 |
+
return {
|
| 58 |
+
"results": results,
|
| 59 |
+
"latency_ms": latency_ms,
|
| 60 |
+
"shards_searched": shards_searched,
|
| 61 |
+
"mode": "Brute Force"
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
def xvector_search(self, query: str) -> Dict[str, Any]:
|
| 65 |
+
"""
|
| 66 |
+
xVector Search (Optimized).
|
| 67 |
+
Uses Router -> Targeted Shard Search.
|
| 68 |
+
"""
|
| 69 |
+
query_vec = self.get_query_embedding(query)
|
| 70 |
+
|
| 71 |
+
start_time = time.time()
|
| 72 |
+
|
| 73 |
+
# 1. Router Prediction
|
| 74 |
+
target_cluster, confidence = self.router.predict(query_vec.reshape(1, -1))
|
| 75 |
+
|
| 76 |
+
# 2. Hybrid Search (Target + Freshness OR Global Fallback)
|
| 77 |
+
results, search_mode = self.db.search_hybrid(query_vec, target_cluster, confidence)
|
| 78 |
+
|
| 79 |
+
end_time = time.time()
|
| 80 |
+
latency_ms = (end_time - start_time) * 1000
|
| 81 |
+
|
| 82 |
+
# Calculate Shards Searched
|
| 83 |
+
if "GLOBAL" in search_mode:
|
| 84 |
+
shards_searched = self.db.num_clusters + 1
|
| 85 |
+
else:
|
| 86 |
+
shards_searched = 2 # Target + Freshness
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"results": results,
|
| 90 |
+
"latency_ms": latency_ms,
|
| 91 |
+
"shards_searched": shards_searched,
|
| 92 |
+
"mode": f"xVector ({search_mode})",
|
| 93 |
+
"confidence": confidence,
|
| 94 |
+
"target_cluster": target_cluster
|
| 95 |
+
}
|
src/data_pipeline.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sentence_transformers import SentenceTransformer
|
| 3 |
+
from datasets import load_dataset
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from typing import List, Union
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
def get_embeddings(model_name: str, texts: List[str]) -> np.ndarray:
|
| 10 |
+
"""
|
| 11 |
+
Loads the specified model and generates embeddings for the given texts.
|
| 12 |
+
Handles 'nomic' and 'qwen' specific requirements (trust_remote_code).
|
| 13 |
+
"""
|
| 14 |
+
print(f"Loading embedding model: {model_name}...")
|
| 15 |
+
|
| 16 |
+
trust_remote_code = False
|
| 17 |
+
if "nomic" in model_name or "qwen" in model_name:
|
| 18 |
+
trust_remote_code = True
|
| 19 |
+
|
| 20 |
+
model = SentenceTransformer(model_name, trust_remote_code=trust_remote_code, device='cpu')
|
| 21 |
+
|
| 22 |
+
# Generate embeddings
|
| 23 |
+
# Convert to numpy array if it returns a tensor or list
|
| 24 |
+
embeddings = model.encode(texts, convert_to_numpy=True, show_progress_bar=True)
|
| 25 |
+
|
| 26 |
+
return embeddings
|
| 27 |
+
|
| 28 |
+
def mrl_slice(vectors: np.ndarray, dims: int) -> np.ndarray:
|
| 29 |
+
"""
|
| 30 |
+
Slices the vectors to the specified dimensions AND applies L2 normalization *after* slicing.
|
| 31 |
+
This is crucial for Matryoshka Representation Learning (MRL).
|
| 32 |
+
"""
|
| 33 |
+
# 1. Slice
|
| 34 |
+
sliced_vectors = vectors[:, :dims]
|
| 35 |
+
|
| 36 |
+
# 2. L2 Normalize
|
| 37 |
+
# Using sklearn's normalize or manual calculation.
|
| 38 |
+
# Manual calculation to avoid extra dependency import inside function if possible,
|
| 39 |
+
# but we have numpy.
|
| 40 |
+
norms = np.linalg.norm(sliced_vectors, axis=1, keepdims=True)
|
| 41 |
+
# Avoid division by zero
|
| 42 |
+
norms[norms == 0] = 1e-10
|
| 43 |
+
normalized_sliced_vectors = sliced_vectors / norms
|
| 44 |
+
|
| 45 |
+
return normalized_sliced_vectors
|
| 46 |
+
|
| 47 |
+
def load_ms_marco(n_samples: int = 1000) -> List[str]:
|
| 48 |
+
"""
|
| 49 |
+
Loads the MS MARCO dataset from Hugging Face.
|
| 50 |
+
Streams the dataset to save RAM.
|
| 51 |
+
Falls back to synthetic data if loading fails.
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
print(f"Attempting to load {n_samples} samples from MS MARCO...")
|
| 55 |
+
dataset = load_dataset("microsoft/ms_marco", "v1.1", split="train", streaming=True)
|
| 56 |
+
|
| 57 |
+
texts = []
|
| 58 |
+
count = 0
|
| 59 |
+
for row in dataset:
|
| 60 |
+
# MS MARCO has 'query' and 'passages'. We'll use passages for the DB.
|
| 61 |
+
# The dataset structure can vary, usually 'passages' is a dict.
|
| 62 |
+
# Let's check the structure or just use a simpler dataset if this is too complex for a quick demo.
|
| 63 |
+
# Actually, let's use the 'query' for simplicity or 'passages' content.
|
| 64 |
+
# For a retrieval engine, we usually index documents.
|
| 65 |
+
# Let's try to get passage text.
|
| 66 |
+
|
| 67 |
+
# Note: ms_marco v1.1 structure:
|
| 68 |
+
# {'query_id': ..., 'query': ..., 'passages': {'is_selected': [...], 'url': [...], 'passage_text': [...]}}
|
| 69 |
+
|
| 70 |
+
if 'passages' in row:
|
| 71 |
+
# Take the first passage text
|
| 72 |
+
passage_list = row['passages']['passage_text']
|
| 73 |
+
if passage_list:
|
| 74 |
+
texts.append(passage_list[0])
|
| 75 |
+
count += 1
|
| 76 |
+
elif 'query' in row:
|
| 77 |
+
# Fallback to queries if passages are weird, but we want documents.
|
| 78 |
+
texts.append(row['query'])
|
| 79 |
+
count += 1
|
| 80 |
+
|
| 81 |
+
if count >= n_samples:
|
| 82 |
+
break
|
| 83 |
+
|
| 84 |
+
if len(texts) < n_samples:
|
| 85 |
+
print("Warning: Could not fetch enough samples from MS MARCO.")
|
| 86 |
+
|
| 87 |
+
return texts
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error loading MS MARCO: {e}")
|
| 91 |
+
print("Falling back to synthetic data.")
|
| 92 |
+
return generate_synthetic_data(n_samples)
|
| 93 |
+
|
| 94 |
+
def generate_synthetic_data(n_samples: int) -> List[str]:
|
| 95 |
+
"""
|
| 96 |
+
Generates synthetic text data for testing.
|
| 97 |
+
"""
|
| 98 |
+
base_sentences = [
|
| 99 |
+
"The quick brown fox jumps over the lazy dog.",
|
| 100 |
+
"Artificial intelligence is transforming the world.",
|
| 101 |
+
"Vector databases enable fast similarity search.",
|
| 102 |
+
"Machine learning models require data for training.",
|
| 103 |
+
"Python is a popular programming language for data science.",
|
| 104 |
+
"Cloud computing provides scalable resources.",
|
| 105 |
+
"Cybersecurity is essential for protecting digital assets.",
|
| 106 |
+
"Blockchain technology ensures decentralized transactions.",
|
| 107 |
+
"Quantum computing will solve complex problems.",
|
| 108 |
+
"Sustainable energy is the future of the planet."
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
data = []
|
| 112 |
+
for i in range(n_samples):
|
| 113 |
+
# Create variations
|
| 114 |
+
base = base_sentences[i % len(base_sentences)]
|
| 115 |
+
data.append(f"{base} Variation {i}")
|
| 116 |
+
|
| 117 |
+
return data
|
src/router.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sklearn.cluster import KMeans
|
| 3 |
+
from sklearn.linear_model import LogisticRegression
|
| 4 |
+
from sklearn.neural_network import MLPClassifier
|
| 5 |
+
import lightgbm as lgb
|
| 6 |
+
from typing import Tuple, Any
|
| 7 |
+
import joblib
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
class LearnedRouter:
|
| 11 |
+
def __init__(self, model_type: str = "lightgbm", n_clusters: int = 32, mrl_dims: int = 64):
|
| 12 |
+
self.model_type = model_type
|
| 13 |
+
self.n_clusters = n_clusters
|
| 14 |
+
self.mrl_dims = mrl_dims
|
| 15 |
+
self.kmeans = None
|
| 16 |
+
self.classifier = None
|
| 17 |
+
|
| 18 |
+
def train(self, X_full: np.ndarray):
|
| 19 |
+
"""
|
| 20 |
+
Trains the router:
|
| 21 |
+
1. Cluster X_full using K-Means to generate ground-truth labels.
|
| 22 |
+
2. Slice X_full to MRL_DIMS.
|
| 23 |
+
3. Train the specified classifier on sliced vectors to predict cluster labels.
|
| 24 |
+
"""
|
| 25 |
+
print(f"Training Router ({self.model_type})...")
|
| 26 |
+
|
| 27 |
+
# 1. Generate Ground Truth Labels with K-Means on FULL vectors
|
| 28 |
+
# (We want the clusters to be based on the high-fidelity data)
|
| 29 |
+
print(" - Running K-Means for ground truth labels...")
|
| 30 |
+
self.kmeans = KMeans(n_clusters=self.n_clusters, random_state=42, n_init=10)
|
| 31 |
+
y_labels = self.kmeans.fit_predict(X_full)
|
| 32 |
+
|
| 33 |
+
# 2. Slice Input Data for the Router
|
| 34 |
+
# The router only sees the low-dim MRL vector
|
| 35 |
+
print(f" - Slicing vectors to {self.mrl_dims} dimensions...")
|
| 36 |
+
# Note: We assume X_full is already normalized if needed,
|
| 37 |
+
# but for MRL slicing we should re-normalize the slice.
|
| 38 |
+
# We'll do a quick slice and normalize here locally or assume caller handles it.
|
| 39 |
+
# Ideally, we use the mrl_slice function from data_pipeline, but to avoid circular imports
|
| 40 |
+
# or dependency issues, let's implement the logic here or import it.
|
| 41 |
+
# Let's do the math here to be self-contained in the class logic.
|
| 42 |
+
X_sliced = X_full[:, :self.mrl_dims]
|
| 43 |
+
norms = np.linalg.norm(X_sliced, axis=1, keepdims=True)
|
| 44 |
+
norms[norms == 0] = 1e-10
|
| 45 |
+
X_train = X_sliced / norms
|
| 46 |
+
|
| 47 |
+
# 3. Train Classifier
|
| 48 |
+
print(f" - Training classifier: {self.model_type}...")
|
| 49 |
+
if self.model_type == "lightgbm":
|
| 50 |
+
# LightGBM
|
| 51 |
+
train_data = lgb.Dataset(X_train, label=y_labels)
|
| 52 |
+
params = {
|
| 53 |
+
'objective': 'multiclass',
|
| 54 |
+
'num_class': self.n_clusters,
|
| 55 |
+
'metric': 'multi_logloss',
|
| 56 |
+
'verbosity': -1,
|
| 57 |
+
'seed': 42
|
| 58 |
+
}
|
| 59 |
+
self.classifier = lgb.train(params, train_data, num_boost_round=100)
|
| 60 |
+
|
| 61 |
+
elif self.model_type == "logistic":
|
| 62 |
+
# Logistic Regression
|
| 63 |
+
self.classifier = LogisticRegression(max_iter=1000, multi_class='multinomial', random_state=42)
|
| 64 |
+
self.classifier.fit(X_train, y_labels)
|
| 65 |
+
|
| 66 |
+
elif self.model_type == "mlp":
|
| 67 |
+
# MLP Classifier
|
| 68 |
+
self.classifier = MLPClassifier(hidden_layer_sizes=(128, 64), max_iter=500, random_state=42)
|
| 69 |
+
self.classifier.fit(X_train, y_labels)
|
| 70 |
+
|
| 71 |
+
else:
|
| 72 |
+
raise ValueError(f"Unknown router model type: {self.model_type}")
|
| 73 |
+
|
| 74 |
+
print("Router training complete.")
|
| 75 |
+
|
| 76 |
+
def predict(self, vector_full: np.ndarray) -> Tuple[int, float]:
|
| 77 |
+
"""
|
| 78 |
+
Predicts the target cluster for a query vector.
|
| 79 |
+
1. Slice input to MRL dims.
|
| 80 |
+
2. Predict probabilities.
|
| 81 |
+
3. Return (best_cluster, confidence_score).
|
| 82 |
+
"""
|
| 83 |
+
# Ensure input is 2D
|
| 84 |
+
if vector_full.ndim == 1:
|
| 85 |
+
vector_full = vector_full.reshape(1, -1)
|
| 86 |
+
|
| 87 |
+
# 1. Slice and Normalize
|
| 88 |
+
X_sliced = vector_full[:, :self.mrl_dims]
|
| 89 |
+
norms = np.linalg.norm(X_sliced, axis=1, keepdims=True)
|
| 90 |
+
norms[norms == 0] = 1e-10
|
| 91 |
+
X_input = X_sliced / norms
|
| 92 |
+
|
| 93 |
+
# 2. Predict
|
| 94 |
+
if self.model_type == "lightgbm":
|
| 95 |
+
probs = self.classifier.predict(X_input) # Returns (n_samples, n_classes)
|
| 96 |
+
elif self.model_type in ["logistic", "mlp"]:
|
| 97 |
+
probs = self.classifier.predict_proba(X_input)
|
| 98 |
+
else:
|
| 99 |
+
raise ValueError("Model not trained or unknown type")
|
| 100 |
+
|
| 101 |
+
# 3. Get best cluster and confidence
|
| 102 |
+
best_cluster = np.argmax(probs, axis=1)[0]
|
| 103 |
+
confidence = np.max(probs, axis=1)[0]
|
| 104 |
+
|
| 105 |
+
return int(best_cluster), float(confidence)
|
| 106 |
+
|
| 107 |
+
def save(self, path: str):
|
| 108 |
+
"""Saves the router (KMeans + Classifier) to disk."""
|
| 109 |
+
print(f"Saving router to {path}...")
|
| 110 |
+
joblib.dump({
|
| 111 |
+
'model_type': self.model_type,
|
| 112 |
+
'n_clusters': self.n_clusters,
|
| 113 |
+
'mrl_dims': self.mrl_dims,
|
| 114 |
+
'kmeans': self.kmeans,
|
| 115 |
+
'classifier': self.classifier
|
| 116 |
+
}, path)
|
| 117 |
+
print("Router saved.")
|
| 118 |
+
|
| 119 |
+
@classmethod
|
| 120 |
+
def load(cls, path: str):
|
| 121 |
+
"""Loads the router from disk."""
|
| 122 |
+
print(f"Loading router from {path}...")
|
| 123 |
+
data = joblib.load(path)
|
| 124 |
+
router = cls(
|
| 125 |
+
model_type=data['model_type'],
|
| 126 |
+
n_clusters=data['n_clusters'],
|
| 127 |
+
mrl_dims=data['mrl_dims']
|
| 128 |
+
)
|
| 129 |
+
router.kmeans = data['kmeans']
|
| 130 |
+
router.classifier = data['classifier']
|
| 131 |
+
print("Router loaded.")
|
| 132 |
+
return router
|
src/vector_db.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from qdrant_client import QdrantClient, models
|
| 3 |
+
from qdrant_client.http.models import Distance, VectorParams
|
| 4 |
+
import numpy as np
|
| 5 |
+
from typing import List, Optional, Dict, Any
|
| 6 |
+
import uuid
|
| 7 |
+
|
| 8 |
+
class UnifiedQdrant:
|
| 9 |
+
def __init__(self, collection_name: str, vector_size: int, num_clusters: int = 32, freshness_shard_id: int = 999):
|
| 10 |
+
self.client = None
|
| 11 |
+
self.collection_name = collection_name
|
| 12 |
+
self.vector_size = vector_size
|
| 13 |
+
self.num_clusters = num_clusters
|
| 14 |
+
self.freshness_shard_id = freshness_shard_id
|
| 15 |
+
|
| 16 |
+
def initialize(self):
|
| 17 |
+
"""
|
| 18 |
+
Connects to Qdrant and sets up the collection with Custom Sharding.
|
| 19 |
+
Handles fallback if Free Tier limits are hit.
|
| 20 |
+
"""
|
| 21 |
+
# Connect
|
| 22 |
+
url = os.getenv("QDRANT_URL", ":memory:")
|
| 23 |
+
api_key = os.getenv("QDRANT_API_KEY", None)
|
| 24 |
+
print(f"Connecting to Qdrant at {url}...")
|
| 25 |
+
self.client = QdrantClient(location=url, api_key=api_key, timeout=60)
|
| 26 |
+
|
| 27 |
+
self.is_local = url == ":memory:" or not url.startswith("http")
|
| 28 |
+
if self.is_local:
|
| 29 |
+
print("WARNING: Running in local/memory mode. Custom Sharding is NOT supported. Simulating behavior.")
|
| 30 |
+
|
| 31 |
+
# Check if collection exists, if so, recreate it for a clean slate (or handle gracefully)
|
| 32 |
+
if self.client.collection_exists(self.collection_name):
|
| 33 |
+
self.client.delete_collection(self.collection_name)
|
| 34 |
+
|
| 35 |
+
# Try to create collection with full clusters
|
| 36 |
+
try:
|
| 37 |
+
self._create_collection_and_shards(self.num_clusters)
|
| 38 |
+
print(f"Successfully created collection with {self.num_clusters} clusters.")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Failed to create {self.num_clusters} clusters: {e}")
|
| 41 |
+
print("Attempting fallback to 8 clusters (Free Tier limit mitigation)...")
|
| 42 |
+
# Fallback 1: 8 Clusters
|
| 43 |
+
try:
|
| 44 |
+
self.num_clusters = 8
|
| 45 |
+
if self.client.collection_exists(self.collection_name):
|
| 46 |
+
self.client.delete_collection(self.collection_name)
|
| 47 |
+
self._create_collection_and_shards(self.num_clusters)
|
| 48 |
+
print(f"Fallback successful: Created collection with {self.num_clusters} clusters.")
|
| 49 |
+
except Exception as e2:
|
| 50 |
+
print(f"Failed to create 8 clusters: {e2}")
|
| 51 |
+
print("CRITICAL: Custom Sharding not supported. Falling back to Standard Collection (No Sharding).")
|
| 52 |
+
# Fallback 2: Standard Collection
|
| 53 |
+
self.num_clusters = 1 # Virtual clusters only
|
| 54 |
+
if self.client.collection_exists(self.collection_name):
|
| 55 |
+
self.client.delete_collection(self.collection_name)
|
| 56 |
+
|
| 57 |
+
self.client.create_collection(
|
| 58 |
+
collection_name=self.collection_name,
|
| 59 |
+
vectors_config=VectorParams(size=self.vector_size, distance=Distance.COSINE)
|
| 60 |
+
)
|
| 61 |
+
print("Fallback successful: Created Standard Collection.")
|
| 62 |
+
|
| 63 |
+
def _create_collection_and_shards(self, n_clusters):
|
| 64 |
+
print(f"Creating collection '{self.collection_name}' with custom sharding ({n_clusters} clusters)...")
|
| 65 |
+
|
| 66 |
+
if self.is_local:
|
| 67 |
+
# Local mode doesn't support sharding_method=CUSTOM
|
| 68 |
+
self.client.create_collection(
|
| 69 |
+
collection_name=self.collection_name,
|
| 70 |
+
vectors_config=VectorParams(size=self.vector_size, distance=Distance.COSINE)
|
| 71 |
+
)
|
| 72 |
+
else:
|
| 73 |
+
self.client.create_collection(
|
| 74 |
+
collection_name=self.collection_name,
|
| 75 |
+
vectors_config=VectorParams(size=self.vector_size, distance=Distance.COSINE),
|
| 76 |
+
sharding_method=models.ShardingMethod.CUSTOM,
|
| 77 |
+
shard_number=n_clusters + 1 # Clusters + Freshness
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# CRITICAL: Create Shard Keys
|
| 81 |
+
if not self.is_local:
|
| 82 |
+
print("Creating shard keys...")
|
| 83 |
+
for i in range(n_clusters):
|
| 84 |
+
self.client.create_shard_key(self.collection_name, str(i))
|
| 85 |
+
|
| 86 |
+
# Create freshness shard key
|
| 87 |
+
self.client.create_shard_key(self.collection_name, str(self.freshness_shard_id))
|
| 88 |
+
print("Shard keys created successfully.")
|
| 89 |
+
|
| 90 |
+
def index_data(self, vectors: np.ndarray, payloads: List[Dict[str, Any]], cluster_ids: List[Optional[int]]):
|
| 91 |
+
"""
|
| 92 |
+
Indexes data into the specific shards based on cluster_ids.
|
| 93 |
+
If cluster_id is None, it goes to the Freshness Shard.
|
| 94 |
+
"""
|
| 95 |
+
points = []
|
| 96 |
+
|
| 97 |
+
# We need to batch this properly, but for simplicity we'll group by shard
|
| 98 |
+
# to minimize network calls if possible, or just iterate.
|
| 99 |
+
# Qdrant's upsert can take a batch, but they must share the same shard key?
|
| 100 |
+
# Actually, with custom sharding, if we provide a list of points,
|
| 101 |
+
# we might need to specify the shard key per operation or batch by shard key.
|
| 102 |
+
# The `upsert` method allows `shard_key_selector`.
|
| 103 |
+
# It's best to batch by shard key.
|
| 104 |
+
|
| 105 |
+
data_by_shard = {}
|
| 106 |
+
|
| 107 |
+
for i, vec in enumerate(vectors):
|
| 108 |
+
cluster_id = cluster_ids[i]
|
| 109 |
+
if cluster_id is None:
|
| 110 |
+
key = str(self.freshness_shard_id)
|
| 111 |
+
else:
|
| 112 |
+
key = str(cluster_id)
|
| 113 |
+
|
| 114 |
+
if key not in data_by_shard:
|
| 115 |
+
data_by_shard[key] = []
|
| 116 |
+
|
| 117 |
+
point_id = str(uuid.uuid4())
|
| 118 |
+
data_by_shard[key].append(
|
| 119 |
+
models.PointStruct(
|
| 120 |
+
id=point_id,
|
| 121 |
+
vector=vec.tolist(),
|
| 122 |
+
payload=payloads[i]
|
| 123 |
+
)
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Upsert batches
|
| 127 |
+
print(f"Indexing data across {len(data_by_shard)} shards...")
|
| 128 |
+
for key, batch_points in data_by_shard.items():
|
| 129 |
+
if self.is_local:
|
| 130 |
+
self.client.upsert(
|
| 131 |
+
collection_name=self.collection_name,
|
| 132 |
+
points=batch_points
|
| 133 |
+
# No shard_key_selector in local
|
| 134 |
+
)
|
| 135 |
+
else:
|
| 136 |
+
self.client.upsert(
|
| 137 |
+
collection_name=self.collection_name,
|
| 138 |
+
points=batch_points,
|
| 139 |
+
shard_key_selector=key
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
def search_hybrid(self, query_vec: np.ndarray, target_cluster: int, confidence: float) -> List[Any]:
|
| 143 |
+
"""
|
| 144 |
+
Performs the hybrid search strategy.
|
| 145 |
+
- Always include FRESHNESS_SHARD_ID.
|
| 146 |
+
- If confidence < 0.5, Global Search (all shards).
|
| 147 |
+
- Else, search [target_cluster, FRESHNESS_SHARD_ID].
|
| 148 |
+
"""
|
| 149 |
+
# Ensure query_vec is list
|
| 150 |
+
if isinstance(query_vec, np.ndarray):
|
| 151 |
+
query_vec = query_vec.tolist()
|
| 152 |
+
if isinstance(query_vec[0], list): # Handle 2D array if passed
|
| 153 |
+
query_vec = query_vec[0]
|
| 154 |
+
|
| 155 |
+
shard_keys = []
|
| 156 |
+
|
| 157 |
+
# Logic
|
| 158 |
+
if confidence < 0.5:
|
| 159 |
+
# Global Search
|
| 160 |
+
# In Qdrant, if we don't specify shard_key_selector, does it search all?
|
| 161 |
+
# With custom sharding, usually yes, or we might need to specify all keys.
|
| 162 |
+
# Let's assume passing None or not passing it searches all.
|
| 163 |
+
# However, the prompt says "Trigger a Global Search".
|
| 164 |
+
# Explicitly, we can just NOT pass shard_key_selector.
|
| 165 |
+
shard_keys = None
|
| 166 |
+
search_mode = "GLOBAL"
|
| 167 |
+
else:
|
| 168 |
+
# Targeted Search
|
| 169 |
+
shard_keys = [str(target_cluster), str(self.freshness_shard_id)]
|
| 170 |
+
search_mode = f"TARGETED (Cluster {target_cluster} + Freshness)"
|
| 171 |
+
|
| 172 |
+
# print(f"Searching: {search_mode} | Confidence: {confidence:.4f}")
|
| 173 |
+
|
| 174 |
+
if self.is_local:
|
| 175 |
+
results = self.client.query_points(
|
| 176 |
+
collection_name=self.collection_name,
|
| 177 |
+
query=query_vec,
|
| 178 |
+
limit=10
|
| 179 |
+
).points
|
| 180 |
+
else:
|
| 181 |
+
results = self.client.query_points(
|
| 182 |
+
collection_name=self.collection_name,
|
| 183 |
+
query=query_vec,
|
| 184 |
+
shard_key_selector=shard_keys,
|
| 185 |
+
limit=10
|
| 186 |
+
).points
|
| 187 |
+
|
| 188 |
+
return results, search_mode
|