Subhadeep Mandal
Fresh deploy
54eb2ce
Raw
History Blame Contribute Delete
7.43 kB
import asyncio
from datetime import datetime, timedelta
from typing import Optional
from qdrant_client import models
from qdrant_client.http.models import Document
from qdrant_client.http.exceptions import ResponseHandlingException
from ...lib.qdrant import get_qdrant_client, reset_qdrant_client, CATALOG_COLLECTION, CATALOG_EMBED_MODEL
from ...core.logger import SingletonLogger
logger = SingletonLogger().get_logger()
async def _qdrant_query(method_name: str, *args, **kwargs):
"""Call a QdrantClient method by name, retrying once on stale connection."""
client = get_qdrant_client()
try:
return await asyncio.to_thread(getattr(client, method_name), *args, **kwargs)
except ResponseHandlingException:
logger.warning("Qdrant connection error, resetting client and retrying")
await asyncio.sleep(2)
client = reset_qdrant_client()
return await asyncio.to_thread(getattr(client, method_name), *args, **kwargs)
def _point_to_dict(point) -> dict:
payload = point.payload or {}
score = getattr(point, "score", None)
return {
"arxiv_id": payload.get("arxiv_id"),
"title": payload.get("title"),
"abstract": payload.get("abstract"),
"authors": payload.get("authors"),
"categories": payload.get("categories"),
"primary_category": payload.get("primary_category"),
"published_date": payload.get("published_date"),
"paper_url": payload.get("paper_url"),
"pdf_url": payload.get("pdf_url"),
"score": score,
"final_score": None,
}
def _exclude(results: list[dict], exclude_ids: set[str]) -> list[dict]:
if not exclude_ids:
return results
return [r for r in results if r.get("arxiv_id") not in exclude_ids]
async def get_similar_papers(
title: str,
abstract: str,
exclude_ids: list[str],
top_k: int = 20,
) -> list[dict]:
results = await _qdrant_query(
"query_points",
collection_name=CATALOG_COLLECTION,
query=Document(
text=f"{title} {abstract}",
model=CATALOG_EMBED_MODEL,
),
using=CATALOG_EMBED_MODEL,
limit=top_k + len(exclude_ids),
with_payload=True,
)
items = [_point_to_dict(p) for p in results.points]
return _exclude(items, set(exclude_ids))[:top_k]
async def get_similar_on_topic(
title: str,
abstract: str,
primary_category: str,
exclude_ids: list[str],
top_k: int = 10,
) -> list[dict]:
results = await _qdrant_query(
"query_points",
collection_name=CATALOG_COLLECTION,
query=Document(
text=f"{title} {abstract}",
model=CATALOG_EMBED_MODEL,
),
using=CATALOG_EMBED_MODEL,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="primary_category",
match=models.MatchValue(value=primary_category),
)
],
),
limit=top_k + len(exclude_ids),
with_payload=True,
)
items = [_point_to_dict(p) for p in results.points]
return _exclude(items, set(exclude_ids))[:top_k]
async def get_papers_by_authors(
authors_str: str,
exclude_ids: list[str],
top_k: int = 10,
) -> list[dict]:
author_list = [a.strip() for a in authors_str.split(";") if a.strip()][:5]
results = []
seen = set(exclude_ids)
for author in author_list:
scroll_filter = models.Filter(
must=[
models.FieldCondition(
key="authors",
match=models.MatchText(text=author),
)
]
)
scroll_result, _ = await _qdrant_query(
"scroll",
collection_name=CATALOG_COLLECTION,
scroll_filter=scroll_filter,
limit=top_k + len(seen),
with_payload=True,
)
for p in scroll_result:
arxiv_id = p.payload.get("arxiv_id")
if arxiv_id and arxiv_id not in seen:
seen.add(arxiv_id)
results.append(_point_to_dict(p))
if len(results) >= top_k:
break
return results[:top_k]
async def get_feed_recommendations(
query_texts: list[str],
category_filter: list[str],
exclude_ids: list[str],
offset: int = 0,
limit: int = 24,
) -> list[dict]:
"""Aggregate vector search across multiple saved papers, filtered by categories."""
combined = " ".join(query_texts)[:2000]
fetch_count = offset + limit + len(exclude_ids)
category_must = []
if category_filter:
category_must.append(
models.FieldCondition(
key="primary_category",
match=models.MatchAny(any=category_filter),
)
)
results = await _qdrant_query(
"query_points",
collection_name=CATALOG_COLLECTION,
query=Document(
text=combined,
model=CATALOG_EMBED_MODEL,
),
using=CATALOG_EMBED_MODEL,
query_filter=models.Filter(must=category_must) if category_must else None,
limit=fetch_count,
with_payload=True,
)
items = [_point_to_dict(p) for p in results.points]
items = _exclude(items, set(exclude_ids))
return items[offset : offset + limit]
async def search_catalog(
query: str,
offset: int = 0,
limit: int = 20,
) -> list[dict]:
"""Free-text vector search over the catalog collection."""
fetch_count = offset + limit
results = await _qdrant_query(
"query_points",
collection_name=CATALOG_COLLECTION,
query=Document(
text=query,
model=CATALOG_EMBED_MODEL,
),
using=CATALOG_EMBED_MODEL,
limit=fetch_count,
with_payload=True,
)
items = [_point_to_dict(p) for p in results.points]
return items[offset : offset + limit]
def rerank_results(
candidates: list[dict],
user_saved_categories: list[str],
) -> list[dict]:
"""Re-rank recommendation candidates by similarity, category affinity, and recency."""
now = datetime.utcnow()
thirty_days_ago = now - timedelta(days=30)
six_months_ago = now - timedelta(days=180)
for candidate in candidates:
similarity = candidate.get("score") or 0.0
category_affinity = 0.0
if candidate.get("primary_category") in user_saved_categories:
category_affinity = 1.0
recency = 0.0
pub_date_str = candidate.get("published_date")
if pub_date_str:
try:
pub_date = datetime.strptime(pub_date_str, "%Y-%m-%d")
if pub_date >= thirty_days_ago:
recency = 1.0
elif pub_date >= six_months_ago:
recency = 0.5
except ValueError:
pass
candidate["final_score"] = (
0.60 * similarity
+ 0.15 * category_affinity
+ 0.15 * recency
+ 0.10 * 0.5
)
candidates.sort(key=lambda c: c.get("final_score", 0), reverse=True)
return candidates