Skip to content

Commit 9dd8cf8

Browse files
committed
refine interop query using cursor
1 parent 20d44d1 commit 9dd8cf8

3 files changed

Lines changed: 95 additions & 56 deletions

File tree

gene_function/models.py

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,38 +50,40 @@ def get_by_gene_and_analysis(pairs):
5050
)
5151

5252
@staticmethod
53-
def get_gene_strain_pairs_paginated(skip: int = 0, limit: int = 10000):
53+
def get_gene_strain_pairs_paginated(after: str | None = None, limit: int = 10000):
5454
"""
55-
Return paginated (gene, genome_id) pairs directly from collection.
56-
No $group - assumes data has no duplicates, InteropDB will upsert anyway.
55+
Return paginated (gene, genome_id) pairs using cursor-based pagination.
56+
Uses _id > after to seek directly, so every page is equally fast.
5757
5858
Args:
59-
skip: Number of records to skip
59+
after: The _id of the last document from the previous page (None for first page)
6060
limit: Max records to return
6161
6262
Returns:
6363
{
6464
"pairs": [...],
65-
"total": int
65+
"next_cursor": str | None (None means no more data)
6666
}
6767
"""
68-
# Get total count using aggregation
69-
count_pipeline = [
70-
{"$match": {"gene": {"$ne": None}, "genome_id": {"$ne": None}}},
71-
{"$count": "total"}
72-
]
73-
count_result = list(GeneInfo.objects.aggregate(count_pipeline))
74-
total = count_result[0]["total"] if count_result else 0
68+
filter_query = {"gene": {"$ne": None}, "genome_id": {"$ne": None}, "locus_tag": {"$ne": None}}
69+
if after is not None:
70+
from bson import ObjectId
71+
filter_query["_id"] = {"$gt": ObjectId(after)}
7572

76-
# Direct find with skip/limit (fast)
7773
cursor = GeneInfo.objects.find(
78-
{"gene": {"$ne": None}, "genome_id": {"$ne": None}, "locus_tag": {"$ne": None}},
79-
projection={"_id": 0, "gene": 1, "genome_id": 1, "locus_tag": 1}
80-
).skip(skip).limit(limit)
74+
filter_query,
75+
projection={"_id": 1, "gene": 1, "genome_id": 1, "locus_tag": 1}
76+
).sort("_id", 1).limit(limit)
77+
78+
pairs = []
79+
last_id = None
80+
for doc in cursor:
81+
pairs.append({"gene": doc["gene"], "strain": doc["genome_id"], "locus_tag": doc["locus_tag"]})
82+
last_id = str(doc["_id"])
8183

82-
pairs = [{"gene": doc["gene"], "strain": doc["genome_id"], "locus_tag": doc["locus_tag"]} for doc in cursor]
84+
next_cursor = last_id if len(pairs) == limit else None
8385

84-
return {"pairs": pairs, "total": total}
86+
return {"pairs": pairs, "next_cursor": next_cursor}
8587

8688
def get_gene_info_and_pangenomic_class_pipeline(gene_match): # This is an ugly workaround to make it compatible with Azure Cosmos DB
8789
return [
@@ -177,16 +179,38 @@ def get_all_strains():
177179
return [doc["genome_id"] for doc in cursor if doc.get("genome_id")]
178180

179181
@staticmethod
180-
def get_all_strains_paginated(skip=0, limit=10000):
182+
def get_all_strains_paginated(after: str | None = None, limit: int = 10000):
181183
"""
182-
Return paginated distinct genome_id values.
183-
Uses distinct() for fast retrieval, then slices in Python.
184+
Return paginated genome_id values using cursor-based pagination.
185+
Uses _id > after to seek directly, so every page is equally fast.
186+
187+
Args:
188+
after: The _id of the last document from the previous page (None for first page)
189+
limit: Max records to return
190+
191+
Returns:
192+
{"strains": [...], "next_cursor": str | None}
184193
"""
185-
col = GenomeInfo.objects.collection
186-
all_ids = sorted(col.distinct("genome_id"))
187-
total = len(all_ids)
188-
page = all_ids[skip:skip + limit]
189-
return {"strains": page, "total": total}
194+
from bson import ObjectId
195+
196+
filter_query = {"genome_id": {"$ne": None}}
197+
if after is not None:
198+
filter_query["_id"] = {"$gt": ObjectId(after)}
199+
200+
cursor = GenomeInfo.objects.find(
201+
filter_query,
202+
projection={"_id": 1, "genome_id": 1}
203+
).sort("_id", 1).limit(limit)
204+
205+
strains = []
206+
last_id = None
207+
for doc in cursor:
208+
strains.append(doc["genome_id"])
209+
last_id = str(doc["_id"])
210+
211+
next_cursor = last_id if len(strains) == limit else None
212+
213+
return {"strains": strains, "next_cursor": next_cursor}
190214

191215

192216
def get_genome_and_isolation_info_pipeline(genome_match):

interop_query/views.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@
4949
)
5050
@api_view(["GET"])
5151
def genes(request):
52-
"""Return all genes with species and PanKB URLs (paginated)."""
52+
"""Return all genes with species and PanKB URLs (cursor-based pagination)."""
5353
try:
54-
skip = int(request.GET.get("skip", 0))
55-
limit = min(int(request.GET.get("limit", 10000)), 50000)
54+
after = request.GET.get("after")
55+
limit = min(int(request.GET.get("limit", 50000)), 50000)
5656

57-
result = GeneAnnotations.get_all_genes_paginated(skip=skip, limit=limit)
57+
result = GeneAnnotations.get_all_genes_paginated(after=after, limit=limit)
5858
gene_list = result["genes"]
5959

6060
for gene_data in gene_list:
@@ -67,10 +67,9 @@ def genes(request):
6767

6868
return Response({
6969
"genes": gene_list,
70-
"total": result["total"],
71-
"skip": skip,
7270
"limit": limit,
73-
"has_more": skip + len(gene_list) < result["total"],
71+
"next_cursor": result["next_cursor"],
72+
"has_more": result["next_cursor"] is not None,
7473
})
7574
except Exception as e:
7675
logger.exception("list_all_genes failed")
@@ -112,12 +111,12 @@ def genes(request):
112111
)
113112
@api_view(["GET"])
114113
def strains(request):
115-
"""Return all strains (genome IDs) with PanKB URLs (paginated)."""
114+
"""Return all strains (genome IDs) with PanKB URLs (cursor-based pagination)."""
116115
try:
117-
skip = int(request.GET.get("skip", 0))
118-
limit = min(int(request.GET.get("limit", 10000)), 50000)
116+
after = request.GET.get("after")
117+
limit = min(int(request.GET.get("limit", 50000)), 50000)
119118

120-
result = GenomeInfo.get_all_strains_paginated(skip=skip, limit=limit)
119+
result = GenomeInfo.get_all_strains_paginated(after=after, limit=limit)
121120
strain_ids = result["strains"]
122121

123122
strain_list = []
@@ -129,10 +128,9 @@ def strains(request):
129128

130129
return Response({
131130
"strains": strain_list,
132-
"total": result["total"],
133-
"skip": skip,
134131
"limit": limit,
135-
"has_more": skip + len(strain_list) < result["total"],
132+
"next_cursor": result["next_cursor"],
133+
"has_more": result["next_cursor"] is not None,
136134
})
137135
except Exception as e:
138136
logger.exception("list_all_strains failed")
@@ -176,12 +174,12 @@ def strains(request):
176174
)
177175
@api_view(["GET"])
178176
def gene_strain_pairs(request):
179-
"""Return distinct (gene, strain, locus_tag) pairs with PanKB URLs."""
177+
"""Return distinct (gene, strain, locus_tag) pairs with PanKB URLs (cursor-based pagination)."""
180178
try:
181-
skip = int(request.GET.get("skip", 0))
182-
limit = min(int(request.GET.get("limit", 10000)), 50000)
179+
after = request.GET.get("after") # _id cursor from previous page
180+
limit = min(int(request.GET.get("limit", 50000)), 50000)
183181

184-
result = GeneInfo.get_gene_strain_pairs_paginated(skip=skip, limit=limit)
182+
result = GeneInfo.get_gene_strain_pairs_paginated(after=after, limit=limit)
185183
pairs = result["pairs"]
186184

187185
for pair in pairs:
@@ -193,10 +191,9 @@ def gene_strain_pairs(request):
193191

194192
return Response({
195193
"pairs": pairs,
196-
"total": result["total"],
197-
"skip": skip,
198194
"limit": limit,
199-
"has_more": skip + len(pairs) < result["total"],
195+
"next_cursor": result["next_cursor"],
196+
"has_more": result["next_cursor"] is not None,
200197
})
201198
except Exception as e:
202199
logger.exception("get_gene_strain_pairs failed")

pangenome_analyses/models.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,39 @@ def get_all_genes():
3030
return result
3131

3232
@staticmethod
33-
def get_all_genes_paginated(skip=0, limit=10000):
33+
def get_all_genes_paginated(after: str | None = None, limit: int = 10000):
3434
"""
35-
Return paginated (gene, pangenome_analysis) pairs.
36-
Uses indexed find+sort+skip+limit for performance on large collections.
35+
Return paginated (gene, pangenome_analysis) pairs using cursor-based pagination.
36+
Uses _id > after to seek directly, so every page is equally fast.
37+
38+
Args:
39+
after: The _id of the last document from the previous page (None for first page)
40+
limit: Max records to return
41+
42+
Returns:
43+
{"genes": [...], "next_cursor": str | None}
3744
"""
45+
from bson import ObjectId
46+
47+
filter_query = {"gene": {"$ne": None}, "pangenome_analysis": {"$ne": None}}
48+
if after is not None:
49+
filter_query["_id"] = {"$gt": ObjectId(after)}
50+
3851
col = GeneAnnotations.objects.collection
3952
cursor = col.find(
40-
{"gene": {"$ne": None}, "pangenome_analysis": {"$ne": None}},
41-
{"_id": 0, "gene": 1, "pangenome_analysis": 1},
42-
).sort([("pangenome_analysis", 1), ("gene", 1)]).skip(skip).limit(limit)
43-
genes = list(cursor)
53+
filter_query,
54+
{"_id": 1, "gene": 1, "pangenome_analysis": 1},
55+
).sort("_id", 1).limit(limit)
56+
57+
genes = []
58+
last_id = None
59+
for doc in cursor:
60+
genes.append({"gene": doc["gene"], "pangenome_analysis": doc["pangenome_analysis"]})
61+
last_id = str(doc["_id"])
4462

45-
total = col.estimated_document_count()
63+
next_cursor = last_id if len(genes) == limit else None
4664

47-
return {"genes": genes, "total": total}
65+
return {"genes": genes, "next_cursor": next_cursor}
4866

4967
def get_gene_analysis_pairs(genes):
5068
"""

0 commit comments

Comments
 (0)