Skip to main content

Searchable pandas text extension arrays for prototyping search

Project description

SearchArray

Python package | Discuss at Relevance Slack

SearchArray turns Pandas string columns into a term index. It alows efficient BM25 / TFIDF scoring of phrases and individual tokens.

Think Lucene, but as a Pandas column.

from searcharray import SearchArray
import pandas as pd

df['title_indexed'] = SearchArray.index(df['title'])
np.sort(df['title_indexed'].array.score(['cat', 'in', 'the', 'hat']))   # Search w/ phrase

> BM25 scores:
> array([ 0.        ,  0.        ,  0.        , ..., 15.84568033, 15.84568033, 15.84568033])

Docs | Guide

SearchArray is documented in these notebooks:

SearchArray Guide | SearchArray Offline Experiment | About internals

Installation

pip install searcharray

Features

  • Search w/ terms by passing a string
  • Search w/ a phrase by passing a list[str]
  • Search w/ a phrase w/ edit-distance by passing slop=N.
  • Access raw stats arrays in termfreqs / docfreqs methods on the array
  • Bring your own tokenizer. Pass any (def tokenize(value: str) -> List[str]) when indexing.
  • Memory map by passing data_dir to index for memory mapped index
  • Accepts any python function to compute similarity. Here's one similarity
  • Scores the entire dataframe, allowing combination w/ other ranking attributes (recency, popularity, etc) or scores from other fields (ie boolean queries)
  • Implement's Solr's edismax query parser for efficient prototyping

Motivation

To simplify lexical search in the Python data stack.

Many ML / AI practitioners reach for a vector search solution, then realize they need to sprinkle in some degree of BM25 / lexical search. Let's get traditional full-text search to behave like other parts of the data stack.

SearchArray creates a Pandas-centric way of creating and using a search index as just part of a Pandas array. In a sense, it builds a search engine in Pandas - to allow anyone to prototype ideas and perform reranking, without external systems.

You can see a full end-to-end search relevance experiment in this colab notebook

IE, let's take a dataframe that has a bunch of text, like movie title and overviews:

In[1]: df = pd.DataFrame({'title': titles, 'overview': overviews}, index=ids)
Out[1]:
                                        title                                           overview
374430          Black Mirror: White Christmas  This feature-length special consists of three ...
19404   The Brave-Hearted Will Take the Bride  Raj is a rich, carefree, happy-go-lucky second...
278                  The Shawshank Redemption  Framed in the 1940s for the double murder of h...
372058                             Your Name.  High schoolers Mitsuha and Taki are complete s...
238                             The Godfather  Spanning the years 1945 to 1955, a chronicle o...
...                                       ...                                                ...
65513                          They Came Back  The lives of the residents of a small French t...
65515                       The Eleventh Hour  An ex-Navy SEAL, Michael Adams, (Matthew Reese...
65521                      Pyaar Ka Punchnama  Outspoken and overly critical Nishant Agarwal ...
32767                                  Romero  Romero is a compelling and deeply moving look ...

Index the text:

In[2]: df['title_indexed'] = SearchArray.index(df['title'])
       df

Out[2]:
                                        title                                           overview                                      title_indexed
374430          Black Mirror: White Christmas  This feature-length special consists of three ...  Terms({'Black', 'Mirror:', 'White'...
19404   The Brave-Hearted Will Take the Bride  Raj is a rich, carefree, happy-go-lucky second...  Terms({'The', 'Brave-Hearted', 'Wi...
278                  The Shawshank Redemption  Framed in the 1940s for the double murder of h...  Terms({'The', 'Shawshank', 'Redemp...
372058                             Your Name.  High schoolers Mitsuha and Taki are complete s...  Terms({'Your', 'Name.'}, {'Your': ...
238                             The Godfather  Spanning the years 1945 to 1955, a chronicle o...  Terms({'The', 'Godfather'}, {'The'...
...                                       ...                                                ...                                                ...
65513                          They Came Back  The lives of the residents of a small French t...  Terms({'Back', 'They', 'Came'},...
65515                       The Eleventh Hour  An ex-Navy SEAL, Michael Adams, (Matthew Reese...  Terms({'The', 'Hour', 'Eleventh': ...
65521                      Pyaar Ka Punchnama  Outspoken and overly critical Nishant Agarwal ...  Terms({'Ka', 'Pyaar', 'Punchnama':...
32767                                  Romero  Romero is a compelling and deeply moving look ...        Terms({'Romero'})
65534                                  Poison  Paul Braconnier and his wife Blandine only hav...        Terms({'Poison'})```

(notice the dumb tokenization - no worries you can pass your own tokenizer).

Then search, getting top N with Cat

In[3]: np.sort(df['title_indexed'].array.score('Cat'))
Out[3]: array([ 0.        ,  0.        ,  0.        , ..., 15.84568033,
                15.84568033, 15.84568033])

In[4]: df['title_indexed'].score('Cat').argsort()
Out[4]: 

array([0, 18561, 18560, ..., 15038, 19012,  4392])

And since its just pandas, we can, of course just retrieve the top matches

In[5]: df.iloc[top_n_cat[-10:]]
Out[5]:
                  title                                           overview                                      title_indexed
24106     The Black Cat  American honeymooners in Hungary are trapped i...  Terms({'Black': 1, 'The': 1, 'Cat': 1}, ...
12593     Fritz the Cat  A hypocritical swinging college student cat ra...  Terms({'Cat': 1, 'the': 1, 'Fritz': 1}, ...
39853  The Cat Concerto  Tom enters from stage left in white tie and ta...  Terms({'The': 1, 'Cat': 1, 'Concerto': 1...
75491   The Rabbi's Cat  Based on the best-selling graphic novel by Joa...  Terms({'The': 1, 'Cat': 1, "Rabbi's": 1}...
57353           Cat Run  When a sexy, high-end escort holds the key evi...  Terms({'Cat': 1, 'Run': 1}, {'Cat': [0],...
25508        Cat People  Sketch artist Irena Dubrovna (Simon) and Ameri...  Terms({'Cat': 1, 'People': 1}, {'Cat': [...
11694        Cat Ballou  A woman seeking revenge for her murdered fathe...  Terms({'Cat': 1, 'Ballou': 1}, {'Cat': [...
25078          Cat Soup  The surreal black comedy follows Nyatta, an an...  Terms({'Cat': 1, 'Soup': 1}, {'Cat': [0]...
35888        Cat Chaser  A Miami hotel owner finds danger when be becom...  Terms({'Cat': 1, 'Chaser': 1}, {'Cat': [...
6217         Cat People  After years of separation, Irina (Nastassja Ki...  Terms({'Cat': 1, 'People': 1}, {'Cat': [...

More use cases can be seen in the colab notebook

Goals

The overall goals are to recreate a lot of the lexical features (term / phrase search) of a search engine like Solr or Elasticsearch, but in a Pandas dataframe.

Memory efficient and fast text index

We want the index to be as memory efficient and fast at searching as possible. We want using it to have a minimal overhead.

We want you to be able to work with a reasonable dataset (100X-1M docs) relatively efficiently for offline evaluation. And 1000s for fast reranking in a service.

Experimentation, reranking, functionality over scalability

Instead of building for 'big data' our goal is to build for small-data. That is, focus on capabilities and expressiveness of Pandas, over limiting functionality in favor of scalability.

To this end, the applications of searcharray will tend to be focused on experimentation and top N candidate reranking. For experimentation, we want any ideas expressed in Pandas to have a somewhat clear path / "contract" in how they'd be implemented in a classical lexical search engine. For reranking, we want to load some top N results from a base system and be able to modify them.

Make lexical search compatible with the data stack

We know in search, RAG, and other retrieval problems hybrid search techniques dominate. Yet often its cast in terms of a giant, weird, big data lexical search engine that looks odd to most data scientists being joined with a vector database. We want lexical search to be more approachable to data scientists and ML engineers building these systems.

Non-goals

You need to bring your own tokenization

Python libraries already do tokenization really well. Even exceeding what Lucene can do... giving you the ability to simulate and/or exceed the abilities of Lucene's tokenization.

In SearchArray, a tokenizer is a function takes a string and emits a series of tokens. IE dumb, default whitespace tokenization:

def ws_tokenizer(string):
    return string.split()

And you can pass any tokenizer that matches this signature to index:

def ws_lowercase_tokenizer(string):
    return string.lower().split()

df['title_indexed'] = SearchArray.index(df['title'], tokenizer=ws_lowercase_tokenizer)

Create your own using stemming libraries, or whatever Python functionality you want.

Use Pandas instead of function queries

Solr has its own unique function query syntaxhttps://solr.apache.org/guide/7_7/function-queries.html. Elasticsearch has Painless.

Instead of recreating these, simply use Pandas on existing Pandas columns. Then later, if you need to implement this in Solr or Elasticsearch, attempt to recreate the functionality. Arguably what's in Solr / ES would be a subset of what you could do in Pandas.

# Calculate the number of hours into the past
df['hrs_into_past'] = (now - df['timestamp']).dt.total_seconds() / 3600

Then multiply by BM25 if you want:

df['score'] = df['title_indexed'].score('Cat') * df['hrs_into_past']

Vector search

We focus on the lexical, ie "BM25-ish" and adjacent problems. There are other great tools for vector search out there.

Need help?

Visit the #searcharray channel on Relevance Slack

TODOs / Future Work / Known issues

  • Always more efficient
  • Support tokenizers with overlapping positions (ie synonyms, etc)
  • Improve support for phrase slop
  • Helper functions (like this start at edismax that help recreate Solr / Elasticsearch lexical queries)
  • Fuzzy search
  • Efficient way to "slurp" some top N results from retrieval system into a dataframe

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

searcharray-0.0.72-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (675.4 kB view details)

Uploaded PyPy manylinux: glibc 2.17+ x86-64

searcharray-0.0.72-pp310-pypy310_pp73-macosx_11_0_arm64.whl (535.2 kB view details)

Uploaded PyPy macOS 11.0+ ARM64

searcharray-0.0.72-pp310-pypy310_pp73-macosx_10_15_x86_64.whl (560.0 kB view details)

Uploaded PyPy macOS 10.15+ x86-64

searcharray-0.0.72-cp313-cp313-musllinux_1_2_x86_64.whl (3.8 MB view details)

Uploaded CPython 3.13 musllinux: musl 1.2+ x86-64

searcharray-0.0.72-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.7 MB view details)

Uploaded CPython 3.13 manylinux: glibc 2.17+ x86-64

searcharray-0.0.72-cp313-cp313-macosx_11_0_arm64.whl (631.3 kB view details)

Uploaded CPython 3.13 macOS 11.0+ ARM64

searcharray-0.0.72-cp313-cp313-macosx_10_13_x86_64.whl (671.6 kB view details)

Uploaded CPython 3.13 macOS 10.13+ x86-64

searcharray-0.0.72-cp312-cp312-musllinux_1_2_x86_64.whl (3.8 MB view details)

Uploaded CPython 3.12 musllinux: musl 1.2+ x86-64

searcharray-0.0.72-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.7 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

searcharray-0.0.72-cp312-cp312-macosx_11_0_arm64.whl (640.0 kB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

searcharray-0.0.72-cp312-cp312-macosx_10_13_x86_64.whl (681.1 kB view details)

Uploaded CPython 3.12 macOS 10.13+ x86-64

searcharray-0.0.72-cp311-cp311-musllinux_1_2_x86_64.whl (3.9 MB view details)

Uploaded CPython 3.11 musllinux: musl 1.2+ x86-64

searcharray-0.0.72-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.8 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

searcharray-0.0.72-cp311-cp311-macosx_11_0_arm64.whl (633.3 kB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

searcharray-0.0.72-cp311-cp311-macosx_10_9_x86_64.whl (674.3 kB view details)

Uploaded CPython 3.11 macOS 10.9+ x86-64

searcharray-0.0.72-cp310-cp310-musllinux_1_2_x86_64.whl (3.6 MB view details)

Uploaded CPython 3.10 musllinux: musl 1.2+ x86-64

searcharray-0.0.72-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.5 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

searcharray-0.0.72-cp310-cp310-macosx_11_0_arm64.whl (634.0 kB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

searcharray-0.0.72-cp310-cp310-macosx_10_9_x86_64.whl (674.2 kB view details)

Uploaded CPython 3.10 macOS 10.9+ x86-64

File details

Details for the file searcharray-0.0.72-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 9914b2883431a8d734ec69befaeca15cd1fe26cfba15d471bb9acefaaa90684e
MD5 8460558c6656efe06f74cbc8359271b5
BLAKE2b-256 99056e7c9531f1b3d58f382fa6143160ca914537eb605c9ac070c0f624ff98cd

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-pp310-pypy310_pp73-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-pp310-pypy310_pp73-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a64efee116aeb2b5f5e9112f858808c457154c6528c2e1a832caecd300f133db
MD5 2e1af87361d845535d872baa9c986bcc
BLAKE2b-256 8a73fed0b4e30665517068769298cad7b1e3c0e38775ee8c797a12edb6a8700a

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-pp310-pypy310_pp73-macosx_10_15_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-pp310-pypy310_pp73-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 bc3a55a7ccd7687817156ebd8a5b2a3530fc974dde88fd4567d8fe3289dda3fd
MD5 0a9993f4022a250576cbf998c2c5e246
BLAKE2b-256 f2f1a8b251092999d9df9bee323729dc9a98383ed215c276879875015bbdd3e9

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp313-cp313-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp313-cp313-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 8a6a02eb6b5fc711e33de51775d745c118f7e5d0dc66b5b167092ab223b44bbb
MD5 b8927e0abfc98a84c2c4a0db7ea74f56
BLAKE2b-256 abc51bda803db08841eb7d97dbb0a191f3782e4f7972316048a1c10bd0f89e2e

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5b6320e116d42443245115ef5357afefcc31175b0332d9a77a14a1d194975c3b
MD5 269ccdaa9423cc6e19896fe241a9371f
BLAKE2b-256 09cd549ad2c46cd172507a41800ea63c36ac24add8dffc8d47da302a89efba16

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 68c0e2c66033072e71ad9b38f77c8d10ed58f61e21c7c96b21be60137a5b6bcb
MD5 bfdaf19cd3f0740125dee4e9ed9fdacf
BLAKE2b-256 9a569fb785843a0f315e7689e15b13b51b3f04df924a8fc14f5c805157e8d11b

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp313-cp313-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp313-cp313-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 f7d64af17f7c3c030934fc8ffd01f2603d94ee6c4325501402d60aba2ab8e0ef
MD5 b569599493ecb91d391bc92f7c3cd576
BLAKE2b-256 b77303acfe8ed7067857399659595ac2fe3f461d26ce2966f2a6074fbc57defd

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp312-cp312-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 a56c2adc4eda5a20661b5fb23226b9a9b1b9ac8a0a45dd54b44c39173ab1bbd4
MD5 e6b42616a4b299c316d8417942ac3496
BLAKE2b-256 93f3fa7d25428b84e4effbb1209ac09a80853109121c11dada61ac036572b5db

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6b3a5dd7a91fb08417488e2d58a9051621198ba8560d396d0e2a0c519aaa35dd
MD5 44bff71981192d1990f840a901e3350e
BLAKE2b-256 26e7c49b5d881ea5a617183ec7bc542c912dff96069eabf6c1259a9e4835f2c7

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ad31b3d1560f54b5a7fab3dd28518eaa70ed982bb6e8a8c01b904b502c5de9df
MD5 38674c8914a4660f4b28de7a7dfc34c5
BLAKE2b-256 511d3b6750a30da76154ada9d3f65d4e1f48f8ed22dadbb0a395a196c77dc082

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp312-cp312-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp312-cp312-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 f5f19a2cc6ebf1823e82d22fb4f869848fd1e729966b4ae988eaf45c51ca76a5
MD5 cc9715e394e314a6693b0d3779dfcc9c
BLAKE2b-256 9088b05e628db51392a6c630d98a1c058f37ee1fe559f0ceea40c08a8eb7514a

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp311-cp311-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 0bbdb939a0172c2b8a75096cc15d1905bc1914b180dde422d24b89f5fe2061fd
MD5 ef0867cafe48877c9dd8593f8ba84218
BLAKE2b-256 3eafedd5c64bd48c366d6564b8c42d73d4bccfa2d60611fca7c24c5cdbd40bc2

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 8c17fbbf94e38290cc3d36e94432e32d3d45d6cafef3b6cb59de7c596c7125f3
MD5 7ff42fc556648206814e1a8174478ea2
BLAKE2b-256 a92d5893fafa21ba726f00a034c30b34c12f122aa4352027ba2c9ca0726250c7

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 3505e77a34bd831693c26feec587d0637e9ab1ae9f0489a29e062d2f3db62bc3
MD5 19272fc2d006f75d55d8e895b8718424
BLAKE2b-256 741c1bd10377451469b7ba06691dc15412be6fcc5c61a08d74cf1a7d80c419f1

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp311-cp311-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp311-cp311-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 4c28df254501c7636bec995215ea9713e8cf6dc8749b0606e6e30ea548b3388d
MD5 e9044a3962bbe48347092fe382520e94
BLAKE2b-256 c6e8e2aeb11de97b02693f99e2cf420e130daf6fa8652ec8ee1a5ae8d3a2a9eb

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp310-cp310-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 ee429977bfd3be4d96baf3d74a773d98688961f1de62790d1273b70600bf873a
MD5 d2bf9bf4ba9bc2c206bafd45d9959e77
BLAKE2b-256 446dcb8f04378f7f0d0e50da1b502deced3168b8907096c924b1fd875f1eff70

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0b827cc956517f7e445587e2c7f2398d03dcc4cd8dbce5eb69758afd63c6e183
MD5 ce65162f0a32944b2aebe7adcd7a87de
BLAKE2b-256 540fc27b711175769b9e173ba0ab885c0c45f12744a075915b57fe175ecd2d8a

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 432b3a4d38793abd07002cd961549a19e5b2c65e209abae23a4d715000f5c374
MD5 a6b786a32e84b2267c9de92ad75c3916
BLAKE2b-256 7ac3206df50d2e4d44ebdfbab8724535f99449f242a78338ff7260b4b520ae11

See more details on using hashes here.

File details

Details for the file searcharray-0.0.72-cp310-cp310-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for searcharray-0.0.72-cp310-cp310-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 4849a87547e4ebd04584fe0aa5c0d960249a4f901f29f50980c3938c1c6aa765
MD5 8efa1fe361cc62588595be3e6ee9a5e7
BLAKE2b-256 192cb2de81e6a3e6755a8c2c7e0a39483b85efaa2f187ccb199c3dac8fa03598

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page