lukasschwab / arxiv.py

Python wrapper for the arXiv API
MIT License
1.11k stars 123 forks source link

Unreliable results: pages from API are unexpectedly empty #43

Closed Ecanlilar closed 3 years ago

Ecanlilar commented 4 years ago

Describe the bug When running the query below, I receive an inconsistent count of results nearly every run. The results below contain the record count generate by the code provided in the "To Reproduce" section. As you can see, I receive wildly different results each time. Is there a parameter setting I can adjust to receive more reliable results?

To Reproduce import arxiv import pandas as pd test = arxiv.query(query="quantum", id_list=[], max_results=None, start = 0, sort_by="relevance", sort_order="descending", prune=True, iterative=False ,max_chunk_results=1000 ) test_df = pd.DataFrame(test) print(len(test_df))

Expected behavior I am expecting a consistent count of results from this query when run back to back (say within a few minutes or so of each other).

Versions

lukasschwab commented 4 years ago

I can repro this without using pandas:

>>> for _ in range(50):
...     test = arxiv.query(query="quantum", id_list=[], max_results=None, start = 0, sort_by="relevance", sort_order="descending", prune=True, iterative=False, max_chunk_results=1000)
...     print(_, len(test))
...
0 5000
1 13000
2 4200
3 3000
4 1000
...

I suspect this has to do with something on arXiv's end rather than something in this client library––some rate limiting, for example––but I need to investigate more.

MohamedAliRashad commented 4 years ago

Any updates on this ?

jannisborn commented 4 years ago

I observed the same issue as @Ecanlilar and would also be interested in a solution. Maybe the time_sleep argument in Search could help? It can't be controlled via query though.

jonas-nothnagel commented 3 years ago

same problem. any updates?

lukasschwab commented 3 years ago

Diagnosis

After some extended testing tonight, I'm confident that this is an issue with the underlying arXiv API and not with this client library; I'll close this issue here accordingly. The team that maintains the arXiv API is focused on building a JSON API to replace the existing Atom API; I'll raise this issue with them.

Unfortunately, I'm not sure what the root cause is on their end, so I don't have a recommendation. If you do want to fork and modify this package to add retries (described below), you might consider a smaller max_chunk_results page size than 1000 to make the retries faster.

The issue: the arXiv API sometimes returns a valid, but empty, feed with a 200 status even though there are entries available for the specified query at the specified offset. More at the bottom of this comment.

Available improvements

This client library can––and perhaps should––be modified to mitigate this issue using retries. arXiv feeds include a opensearch:totalResults property indicating the total number of entries corresponding to the query; Search._get_next should

  1. Pull this property to use it to limit pagination: n_left = min(self.max_results, <totalResults from feed>)
  2. Retry if n_left > 0 but results is an empty list.

I spotted an unrelated bug in _prune_result which I'll fix shortly.

I'm actually inclined to clean up this client more deeply, which will probably lead to a 1.0.0 release (and perhaps an interface that'll play nicer with the new JSON API when it's released).

Testing

I tested using the query I constructed earlier in this issue:

import arxiv
test = arxiv.query(query="quantum", id_list=[], max_results=None, start = 0, sort_by="relevance", sort_order="descending", prune=True, iterative=False, max_chunk_results=1000)

I modified two functions to shed some light on why _get_next stopped iterating:

In one such run, I got an empty 200 response at start=6000:

{'bozo': False, 'entries': [], 'feed': {'links': [{'href': 'http://arxiv.org/api/query?search_query%3Dquantum%26id_list%3D%26start%3D6000%26max_results%3D1000', 'rel': 'self', 'type': 'application/atom+xml'}], 'title': 'ArXiv Query: search_query=quantum&amp;id_list=&amp;start=6000&amp;max_results=1000', 'title_detail': {'type': 'text/html', 'language': None, 'base': 'http://export.arxiv.org/api/query?search_query=quantum&id_list=&start=6000&max_results=1000&sortBy=relevance&sortOrder=descending', 'value': 'ArXiv Query: search_query=quantum&amp;id_list=&amp;start=6000&amp;max_results=1000'}, 'id': 'http://arxiv.org/api/U9c7OUmEOZDvAXlaxzJl09rG9z0', 'guidislink': True, 'link': 'http://arxiv.org/api/U9c7OUmEOZDvAXlaxzJl09rG9z0', 'updated': '2021-04-02T00:00:00-04:00', 'updated_parsed': time.struct_time(tm_year=2021, tm_mon=4, tm_mday=2, tm_hour=4, tm_min=0, tm_sec=0, tm_wday=4, tm_yday=92, tm_isdst=0), 'opensearch_totalresults': '320665', 'opensearch_startindex': '6000', 'opensearch_itemsperpage': '1000'}, 'headers': {'date': 'Fri, 02 Apr 2021 04:41:09 GMT', 'server': 'Apache', 'access-control-allow-origin': '*', 'vary': 'Accept-Encoding,User-Agent', 'content-encoding': 'gzip', 'content-length': '412', 'connection': 'close', 'content-type': 'application/atom+xml; charset=UTF-8'}, 'href': 'http://export.arxiv.org/api/query?search_query=quantum&id_list=&start=6000&max_results=1000&sortBy=relevance&sortOrder=descending', 'status': 200, 'encoding': 'UTF-8', 'version': 'atom10', 'namespaces': {'': 'http://www.w3.org/2005/Atom', 'opensearch': 'http://a9.com/-/spec/opensearch/1.1/'}}

But re-calling _parse yielded 1000 entries. In this case, a retry would continue _get_next's iteration.

lukasschwab commented 3 years ago

Did some more work on this tonight.

Anecdotally, retries (and other weird behavior like partial pages) seems to happen more with large page size; reducing the page size from 1000 to 100 makes this issue hard to reproduce. Hope that's helpful!

I've started sketching out a v1.0.0 client that adds retries; in my cursory testing so far, a small number of retries (default: 3) seems to make this behave more robustly.

That sketch is here: https://github.com/lukasschwab/arxiv.py/tree/v1.0.0-rewrite

But beware:

Thanks for the input on this issue; I think this'll lead to a meaningful improvement in this package 😁

lukasschwab commented 3 years ago

v1.0.0 is released, and it implements retries! https://github.com/lukasschwab/arxiv.py/releases/tag/1.0.0

Cheers

jonas-nothnagel commented 3 years ago

Hi @lukasschwab,

first of all. Wow. Thank you so much for this comprehensive update and new release. Really great to see how much work you put in this and how well you document it!

I am now testing the 1.0.1 release and encounter following issue with a minimum working example:

import arxiv 

def query_arxiv(string_query):
    search = arxiv.Search(
        query=string_query,
        #max_results=20000,)
    return search

for i in range(0,5):
    print("try:", i)
    search = query_arxiv("abs:disaster risk management")

If I set max_results to a small value it works well and the the results are consistent. However, for values >1000, or for setting no value at all manually, I always run into Page of results was unexpectedly empty.

I feel this was already answered somewhere else, but how to best query 10000s of results with the new release?

For example for leaving max_results unspecified I run into:

image

lukasschwab commented 3 years ago

@jonas-nothnagel I think you're successfully fetching your 20,000 results! I just need to make the logging clearer. (Opened #56)

Each of those log lines is written from UnexpectedEmptyPageError.__init__. The error is constructed, but it is only raised if all retries are exhausted: https://github.com/lukasschwab/arxiv.py/blob/bb625a215a0cb6a284815380912b85634bf049d6/arxiv/arxiv.py#L374-L385

If all the retries are exhausted and the error is raised, then the generator will stop producing results and you'll see the full exception logged to the console. No more pages will be fetched.

The logs you're seeing say this: the API sometimes sends you empty pages, but the retried requests are succeeding! Otherwise the whole thing would stop.

Does that make sense?

If you'd like, you can log the results as you go along to see that they're still being fetched:

import arxiv

generator = arxiv.Search("abs:disaster risk management", max_results=20000).get()
results = []

for result in generator:
  print("Got result:", result.entry_id)
  results.append(result)

When I improve the logging it'll be easier to see the underlying requests as they happen.

bilalazhar72 commented 1 year ago

I observed the same issue as @Ecanlilar and would also be interested in a solution. Maybe the time_sleep argument in Search could help? It can't be controlled via query though.

in my script i tried time.sleep still same issue

lukasschwab commented 1 year ago

@bilalazhar72 see #129. I recommend upgrading to the v2.0.0 client if you haven't already.

vaish30 commented 8 months ago

@lukasschwab Hi, I have successfully implemented the code to seek the results using the wrapper, when I ran after 2 weeks, it's not giving the data and giving blank response, could you suggest what could be the issue, below is the my code:

import datetime import pandas as pd import time from concurrent.futures import ThreadPoolExecutor

start_date = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc) end_date = datetime.datetime(2024, 1, 4, 23, 59, 59, tzinfo=datetime.timezone.utc)

search = arxiv.Search( query="cat:cs.AI OR cat:stat.ML", sort_by=arxiv.SortCriterion.SubmittedDate, max_results=1000 # Set a default value for max_results )

titles = [] authors_list = [] affiliations_list = []
categories_list = [] published_dates = [] pdf_links = []

batch_size = 50
retry_attempts = 3

def fetch_results(offset): try: search.start = offset results = client.results(search)

    for r in results:
        published_date = r.published

        if start_date <= published_date.replace(tzinfo=datetime.timezone.utc) <= end_date:
            titles.append(r.title)

            authors = ", ".join([author.name for author in r.authors])
            authors_list.append(authors)

            affiliations = ", ".join([author.affiliation if hasattr(author, 'affiliation') else '' for author in r.authors])
            affiliations_list.append(affiliations)

            categories = ", ".join(r.categories)
            categories_list.append(categories)

            published_dates.append(published_date)

            pdf_links.append(r.pdf_url)

except Exception as e:
    print(f"An error occurred: {e}")

total_batches = (search.max_results or 1000) // batch_size + 1

with ThreadPoolExecutor(max_workers=5) as executor:

Submit tasks to fetch results for each batch

for offset in range(0, total_batches * batch_size, batch_size):
    executor.submit(fetch_results, offset)

data = { 'Title': titles, 'Authors': authors_list, 'Affiliations': affiliations_list,
'Categories' : categories_list, 'Published Date': published_dates, 'PDF Link': pdf_links }

df = pd.DataFrame(data)

lukasschwab commented 8 months ago

@vaish30 wrong place for this — responding here: https://github.com/lukasschwab/arxiv.py/issues/155