in Product Management

Understanding the basics of text correction in the context of Ecommerce search

Search is a key feature of any Ecommerce marketplace as it drives organic orders based on buyer intent. Quite often, buyers often mistype their word which return inaccurate/wrong products. Automatic text correction of search queries can help to remedy this situation by showing buyers the results of the intended keyword instead of the mistyped keyword

As a new Search Product Manager at Shopee, I thought it would be good if I could gain better insight into the fundamentals of automatic text correction.

Basics of Text Correction

Peter Norvig has an excellent article highlighting the basics of text correction. In a nutshell, a basic text correction has to have at least 3 components:

Candidate Model

The candidate model generates the list of potential correct words for a given search keyword. Potential candidates are generally generated by doing all possible permutations of adding, substituting, transposing, or deleting a character from the original search keyword, within a given Damerau–Levenshtein edit distance. The

What is the Damerau–Levenshtein edit distance? This is basically the measure of how many operations (adding, substituting, transposing or deleting a character) is needed to convert one sequence to another. As part of his research, Damerau discovered that 80% of human misspellings has an edit distance of 1 (only 1 character change needed).

At this stage, while a lot of candidates will be generated, not all of them are meaningful. The candidates have to be checked against a corpus of known valid words.

Language Model

The language model is basically a corpus of known valid words language and the frequency/probability of such words appearing. i.e. the words “the” appears in 7% of English sentences. Typically, we can get a rough sample model from text mining a large amount of literature and then coming up with this model.

Selection Criteria

The selection criteria is what we use to to decide which of the potential candidates we have found is the “best” word to use as the corrected version. A possible selection criteria would be to compute a score based on the edit distance (the lower the better) and how frequently the word appears in our language model (the higher the better) and to pick the candidate word with the highest score.

Symmetric Delete Spelling Correction

While Peter Norvig’s version of text correction is short and simple, its performance is well known to be quite slow. While reviewing algorithm literature, I came across the Symmetric Delete Spelling Correction algorithm, which claims to be 1000x faster.

I implemented this algorithm in Python and compared the accuracy/speed of it against Peter Norvig’s code.

In [1]:
from pyxdameraulevenshtein import damerau_levenshtein_distance
# The frequent dictionary was obtained from https://github.com/wolfgarbe/symspell and is the intersection 
# of Google Books Ngram Data and Spell Checker Oriented Word Lists
# Download it directly here https://github.com/wolfgarbe/SymSpell/blob/master/SymSpell/frequency_dictionary_en_82_765.txt
fname = "frequency_dictionary_en_82_765.txt"
dictionary = {}

def get_deletes_list(w, delete_distance):
    """to return all possible combinations of the string with x number of deletes"""
    master_deletes_list = []
    queue = [w]
    
    for x in range(delete_distance):
        temporary_queue = []
        
        for word in queue:
            if len(word)>delete_distance:
                for c in range(len(word)):
                    word_minus_c = word[:c] + word[c+1:]
                    if word_minus_c not in master_deletes_list:
                        master_deletes_list.append(word_minus_c)
                    if word_minus_c not in temporary_queue:
                        temporary_queue.append(word_minus_c)
        queue = temporary_queue
        
    return master_deletes_list

    
def create_dictionary_entry(w,frequency):
    """
    the dictionary will be in the format of {"key":[[autocorrected word 1, autocorrected word 2, etc],frequency}
    """
    #Add the word
    if w not in dictionary:
        dictionary[w] = [[w],frequency]   
    else:
        dictionary[w][0].append(w)
        dictionary[w] = [dictionary[w][0],frequency]  
    deletes_list = get_deletes_list(w,2)

    for word in deletes_list:
        if word not in dictionary:
            dictionary[word] = [[w],0]
        else:
            dictionary[word][0].append(w)

def create_dictionary(fname):
    total_frequency =0
    with open(fname) as file:
        for line in file:
            create_dictionary_entry(line.split()[0],line.split()[1])
    for keyword in dictionary:
        total_frequency += float(dictionary[keyword][1])
    for keyword in dictionary:
        dictionary[keyword].append(float(dictionary[keyword][1])/float(total_frequency))


def get_suggestions(w):
    search_deletes_list = get_deletes_list(w,2)
    search_deletes_list.append(w)
    candidate_corrections = []
    
    #Does not correct words which are existing in the dictionary and that has a high frequency based on the word corpus
    if w in dictionary and int(dictionary[w][1])>10000:
        return w
    else:
        for query in search_deletes_list:
            if query in dictionary:
                for suggested_word in dictionary[query][0]:
                        edit_distance = float(damerau_levenshtein_distance(w, suggested_word))
                        frequency = float(dictionary[suggested_word][1])
                        probability = float(dictionary[suggested_word][2])
                        score = frequency*(0.003**(edit_distance))
                        
                        candidate_corrections.append((suggested_word,frequency,edit_distance,score))

        candidate_corrections = sorted(candidate_corrections, key=lambda x:int(x[3]), reverse=True)
        return candidate_corrections

    
def get_correction(w):
    
    try:
        return get_suggestions(w)[0][0]
    except:
        return "no result"

    
create_dictionary(fname)
    

As you can see, the text correction script works reasonably well.

In [2]:
print(get_correction("pphoone"))
print(get_correction("dresss"))
print(get_correction("alptop"))
print(get_correction("beaautifol"))
phone
dress
laptop
beautiful

However, its main improvement over Peter Norvig’s algorithm is its speed in doing text corrections. Below I have combined the same test set of queries used by Peter Norvig to test the algorithm and its speed improvement is nearly 90x-100x faster. Symmetric Delete Spelling Correction allows me process around 1000 to 2000 words per second as compared to Peter Norvig’s method of only 20-25 word per second.

In [3]:
def spelltest(tests):
    "Run correction(wrong) on all (right, wrong) pairs; report results."
    import time
    start = time.clock()
    good, unknown = 0, 0
    n = len(tests)
    wrong_words_list =[]
    for right, wrong in tests:
        w = get_correction(wrong)
        good += (w == right)
        if w!=right:
            wrong_words_list.append(wrong)
        
    dt = time.clock() - start
    print('{:.0%} of {} correct  at {:.0f} words per second '
          .format(good / n, n, n / dt))
#     return wrong_words_list
    
def Testset(lines):
    "Parse 'right: wrong1 wrong2' lines into [('right', 'wrong1'), ('right', 'wrong2')] pairs."
    return [(right, wrong)
            for (right, wrongs) in (line.split(':') for line in lines)
            for wrong in wrongs.split()]

spelltest(Testset(open('norvig-spell-testset1.txt'))) # Development set
spelltest(Testset(open('norvig-spell-testset2.txt'))) # Final test set
74% of 270 correct  at 1075 words per second 
74% of 400 correct  at 1745 words per second 

Below is Norvig’s Code

In [4]:
import re
from collections import Counter

def words(text): return re.findall(r'\w+', text.lower())

WORDS = Counter(words(open('big.txt').read()))

def P(word, N=sum(WORDS.values())): 
    "Probability of `word`."
    return WORDS[word] / N

def correction(word): 
    "Most probable spelling correction for word."
    return max(candidates(word), key=P)

def candidates(word): 
    "Generate possible spelling corrections for word."
    return (known([word]) or known(edits1(word)) or known(edits2(word)) or [word])

def known(words): 
    "The subset of `words` that appear in the dictionary of WORDS."
    return set(w for w in words if w in WORDS)

def edits1(word):
    "All edits that are one edit away from `word`."
    letters    = 'abcdefghijklmnopqrstuvwxyz'
    splits     = [(word[:i], word[i:])    for i in range(len(word) + 1)]
    deletes    = [L + R[1:]               for L, R in splits if R]
    transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]
    replaces   = [L + c + R[1:]           for L, R in splits if R for c in letters]
    inserts    = [L + c + R               for L, R in splits for c in letters]
    return set(deletes + transposes + replaces + inserts)

def edits2(word): 
    "All edits that are two edits away from `word`."
    return (e2 for e1 in edits1(word) for e2 in edits1(e1))



def unit_tests():
    assert correction('speling') == 'spelling'              # insert
    assert correction('korrectud') == 'corrected'           # replace 2
    assert correction('bycycle') == 'bicycle'               # replace
    assert correction('inconvient') == 'inconvenient'       # insert 2
    assert correction('arrainged') == 'arranged'            # delete
    assert correction('peotry') =='poetry'                  # transpose
    assert correction('peotryy') =='poetry'                 # transpose + delete
    assert correction('word') == 'word'                     # known
    assert correction('quintessential') == 'quintessential' # unknown
    assert words('This is a TEST.') == ['this', 'is', 'a', 'test']
    assert Counter(words('This is a test. 123; A TEST this is.')) == (
           Counter({'123': 1, 'a': 2, 'is': 2, 'test': 2, 'this': 2}))
    assert len(WORDS) == 32192
    assert sum(WORDS.values()) == 1115504
    assert WORDS.most_common(10) == [
     ('the', 79808),
     ('of', 40024),
     ('and', 38311),
     ('to', 28765),
     ('in', 22020),
     ('a', 21124),
     ('that', 12512),
     ('he', 12401),
     ('was', 11410),
     ('it', 10681)]
    assert WORDS['the'] == 79808
    assert P('quintessential') == 0
    assert 0.07 < P('the') < 0.08
    return 'unit_tests pass'

def spelltest_norvig(tests, verbose=False):
    "Run correction(wrong) on all (right, wrong) pairs; report results."
    import time
    start = time.clock()
    good, unknown = 0, 0
    n = len(tests)
    for right, wrong in tests:
        w = correction(wrong)
        good += (w == right)
        if w != right:
            unknown += (right not in WORDS)
            if verbose:
                print('correction({}) => {} ({}); expected {} ({})'
                      .format(wrong, w, WORDS[w], right, WORDS[right]))
    dt = time.clock() - start
    print('{:.0%} of {} correct ({:.0%} unknown) at {:.0f} words per second '
          .format(good / n, n, unknown / n, n / dt))
    
def Testset_norvig(lines):
    "Parse 'right: wrong1 wrong2' lines into [('right', 'wrong1'), ('right', 'wrong2')] pairs."
    return [(right, wrong)
            for (right, wrongs) in (line.split(':') for line in lines)
            for wrong in wrongs.split()]

spelltest_norvig(Testset_norvig(open('norvig-spell-testset1.txt'))) # Development set
spelltest_norvig(Testset_norvig(open('norvig-spell-testset2.txt'))) # Final test set
75% of 270 correct (6% unknown) at 28 words per second 
68% of 400 correct (11% unknown) at 24 words per second 

 

Most of the speed gains came because some of the processing was shifted to the pre-query stage and also because the Symmetric Delete Spelling Correction algorithm, as its name suggests, only makes use of deletes. This makes it much less operation intensive.

 

 Improvements and Ecommerce adaptation

While the text correction algorithm created earlier works quite well for general English text correction, it will perform poorly in an Ecommerce setting. This is because its dictionary corpus is not optimised for product names and only contains general English words. In Ecommerce search, the dictionary should be augmented with either product titles or common search queries.

Furthermore, for non-alphabetic languages such as Mandarin or Thai, you will need to design separate text correction algorithms for it.

 

Write a Comment

Comment

  1. Great post, some comments though:
    1. You are comparing the two algorithms using different dictionaries. Thus the speed/precisions differences are not only influenced by the algorithms and their implementation, but also by dictionary quality and dictionary size. Dictionaries are interchangeable between algorithms.
    2. You did the comparison for maximum edit distance=2. The performance difference between the algorithms increases dramatically for higher edit distances.
    3. You implemented the first version of the Symmetric Delete Spelling Correction algorithm. Version 6.1 is much faster, has shorter precalculation time and lower memory consumption.
    See https://towardsdatascience.com/symspell-vs-bk-tree-100x-faster-fuzzy-string-search-spell-checking-c4f10d80a078 for a benchmark between Norvig’s algorithm, BK-Tree and the current version of SymSpell for different edit distances and dictionary sizes.

Webmentions

  • https://bit.ly/patsany-4-sezon-vse-seriy-patsany-4-sezon-2024 March 5, 2018

    https://bit.ly/patsany-4-sezon-vse-seriy-patsany-4-sezon-2024

    https://bit.ly/patsany-4-sezon-vse-seriy-patsany-4-sezon-2024

  • generic name for lyrica March 5, 2018

    generic name for lyrica

    generic name for lyrica

  • amoxicillin for sore throat March 5, 2018

    amoxicillin for sore throat

    amoxicillin for sore throat

  • doxycycline for uti March 5, 2018

    doxycycline for uti

    doxycycline for uti

  • lisinopril in african american March 5, 2018

    lisinopril in african american

    lisinopril in african american

  • is ciprofloxacin good for sinus infection March 5, 2018

    is ciprofloxacin good for sinus infection

    is ciprofloxacin good for sinus infection

  • should i skip metformin when drinking alcohol March 5, 2018

    should i skip metformin when drinking alcohol

    should i skip metformin when drinking alcohol

  • can you take flagyl while pregnant March 5, 2018

    can you take flagyl while pregnant

    can you take flagyl while pregnant

  • valacyclovir interactions with alcohol March 5, 2018

    valacyclovir interactions with alcohol

    valacyclovir interactions with alcohol

  • cephalexin 500 mg capsule March 5, 2018

    cephalexin 500 mg capsule

    cephalexin 500 mg capsule

  • dosage for ampicillin in dogs March 5, 2018

    dosage for ampicillin in dogs

    dosage for ampicillin in dogs

  • what is keflex for March 5, 2018

    what is keflex for

    what is keflex for

  • reasons to prescribe provigil March 5, 2018

    reasons to prescribe provigil

    reasons to prescribe provigil

  • hydroxyzine and trazodone March 5, 2018

    hydroxyzine and trazodone

    hydroxyzine and trazodone

  • prednisone and ibuprofen March 5, 2018

    prednisone and ibuprofen

    prednisone and ibuprofen

  • how does gabapentin work March 5, 2018

    how does gabapentin work

    how does gabapentin work

  • bit.ly/kto-takoy-opsuimolog March 5, 2018

    bit.ly/kto-takoy-opsuimolog

    bit.ly/kto-takoy-opsuimolog

  • batmanapollo.ru March 5, 2018

    batmanapollo.ru

    batmanapollo.ru

  • https://bit.ly/patsany-4-sezon-2024 March 5, 2018

    https://bit.ly/patsany-4-sezon-2024

    https://bit.ly/patsany-4-sezon-2024

  • ivermectin topical March 5, 2018

    ivermectin topical

    ivermectin topical

  • sildenafil March 5, 2018

    sildenafil

    sildenafil

  • stromectol cost March 5, 2018

    stromectol cost

    stromectol cost

  • ivermectin 9mg March 5, 2018

    ivermectin 9mg

    ivermectin 9mg

  • combitic global caplet tadalafil March 5, 2018

    combitic global caplet tadalafil

    combitic global caplet tadalafil

  • ivermectin 200mg March 5, 2018

    ivermectin 200mg

    ivermectin 200mg

  • vardenafil sublingual tablets March 5, 2018

    vardenafil sublingual tablets

    vardenafil sublingual tablets

  • tadalafil vardenafil March 5, 2018

    tadalafil vardenafil

    tadalafil vardenafil

  • viagra over the counter uk March 5, 2018

    viagra over the counter uk

    viagra over the counter uk

  • ivermectin where to buy for humans March 5, 2018

    ivermectin where to buy for humans

    ivermectin where to buy for humans

  • generic viagra 2019 March 5, 2018

    generic viagra 2019

    generic viagra 2019

  • stromectol xl March 5, 2018

    stromectol xl

    stromectol xl

  • differin gel online pharmacy March 5, 2018

    differin gel online pharmacy

    differin gel online pharmacy

  • levitra over the counter March 5, 2018

    levitra over the counter

    levitra over the counter

  • online pharmacy levitra March 5, 2018

    online pharmacy levitra

    online pharmacy levitra

  • mexican pharmacy online medications March 5, 2018

    mexican pharmacy online medications

    mexican pharmacy online medications

  • sildenafil prescription March 5, 2018

    sildenafil prescription

    sildenafil prescription

  • tadalafil (cialis) March 5, 2018

    tadalafil (cialis)

    tadalafil (cialis)

  • cialis online pills March 5, 2018

    cialis online pills

    cialis online pills

  • sildenafil 20 mg dosage for erectile dysfunction March 5, 2018

    sildenafil 20 mg dosage for erectile dysfunction

    sildenafil 20 mg dosage for erectile dysfunction

  • russian-federation March 5, 2018

    russian-federation

    russian-federation

  • ivermectin rx March 5, 2018

    ivermectin rx

    ivermectin rx

  • tizanidine for fibromyalgia March 5, 2018

    tizanidine for fibromyalgia

    tizanidine for fibromyalgia

  • aspirin synthroid March 5, 2018

    aspirin synthroid

    aspirin synthroid

  • coming off venlafaxine 75 mg March 5, 2018

    coming off venlafaxine 75 mg

    coming off venlafaxine 75 mg

  • how often to apply voltaren gel March 5, 2018

    how often to apply voltaren gel

    how often to apply voltaren gel

  • linagliptin vs sitagliptin dose conversion March 5, 2018

    linagliptin vs sitagliptin dose conversion

    linagliptin vs sitagliptin dose conversion

  • should i stop taking tamsulosin March 5, 2018

    should i stop taking tamsulosin

    should i stop taking tamsulosin

  • spironolactone and ibuprofen March 5, 2018

    spironolactone and ibuprofen

    spironolactone and ibuprofen

  • remeron 15 mg for sleep March 5, 2018

    remeron 15 mg for sleep

    remeron 15 mg for sleep

  • doxepin lactosa March 5, 2018

    doxepin lactosa

    doxepin lactosa

  • how long does robaxin take to work March 5, 2018

    how long does robaxin take to work

    how long does robaxin take to work

  • protonix drug interactions March 5, 2018

    protonix drug interactions

    protonix drug interactions

  • jardiance semaglutide March 5, 2018

    jardiance semaglutide

    jardiance semaglutide

  • acarbose source March 5, 2018

    acarbose source

    acarbose source

  • lambda max of repaglinide March 5, 2018

    lambda max of repaglinide

    lambda max of repaglinide

  • drinking on abilify March 5, 2018

    drinking on abilify

    drinking on abilify

  • list March 5, 2018

    list

    list

  • celecoxib warnings March 5, 2018

    celecoxib warnings

    celecoxib warnings

  • how long does it take celexa to work March 5, 2018

    how long does it take celexa to work

    how long does it take celexa to work

  • ashwagandha organic india March 5, 2018

    ashwagandha organic india

    ashwagandha organic india

  • buspar vs klonopin March 5, 2018

    buspar vs klonopin

    buspar vs klonopin

  • celebrex 200 mg used for March 5, 2018

    celebrex 200 mg used for

    celebrex 200 mg used for

  • bupropion and sertraline combination treatment dosage March 5, 2018

    bupropion and sertraline combination treatment dosage

    bupropion and sertraline combination treatment dosage

  • uloric vs allopurinol March 5, 2018

    uloric vs allopurinol

    uloric vs allopurinol

  • baby aspirin March 5, 2018

    baby aspirin

    baby aspirin

  • why does amitriptyline cause breast enlargement March 5, 2018

    why does amitriptyline cause breast enlargement

    why does amitriptyline cause breast enlargement

  • aripiprazole drug interactions March 5, 2018

    aripiprazole drug interactions

    aripiprazole drug interactions

  • what is the usual dosage for effexor March 5, 2018

    what is the usual dosage for effexor

    what is the usual dosage for effexor

  • generic for cozaar March 5, 2018

    generic for cozaar

    generic for cozaar

  • diclofenac sodium gel March 5, 2018

    diclofenac sodium gel

    diclofenac sodium gel

  • what does flexeril look like March 5, 2018

    what does flexeril look like

    what does flexeril look like

  • how long should you take flomax March 5, 2018

    how long should you take flomax

    how long should you take flomax

  • simvastatin and ezetimibe in aortic stenosis seas study March 5, 2018

    simvastatin and ezetimibe in aortic stenosis seas study

    simvastatin and ezetimibe in aortic stenosis seas study

  • depakote 500 mg twice a day March 5, 2018

    depakote 500 mg twice a day

    depakote 500 mg twice a day

  • contrave not working anymore March 5, 2018

    contrave not working anymore

    contrave not working anymore

  • diltiazem and alcohol March 5, 2018

    diltiazem and alcohol

    diltiazem and alcohol

  • side effects of reducing citalopram from 20mg to 10mg March 5, 2018

    side effects of reducing citalopram from 20mg to 10mg

    side effects of reducing citalopram from 20mg to 10mg

  • augmentin for strep throat March 5, 2018

    augmentin for strep throat

    augmentin for strep throat

  • ddavp and fluid restriction March 5, 2018

    ddavp and fluid restriction

    ddavp and fluid restriction

  • how long do amoxicillin side effects last March 5, 2018

    how long do amoxicillin side effects last

    how long do amoxicillin side effects last

  • does bactrim treat strep throat March 5, 2018

    does bactrim treat strep throat

    does bactrim treat strep throat

  • bactrim yeast infection March 5, 2018

    bactrim yeast infection

    bactrim yeast infection

  • ciprofloxacin c diff March 5, 2018

    ciprofloxacin c diff

    ciprofloxacin c diff

  • cephalexin interactions March 5, 2018

    cephalexin interactions

    cephalexin interactions

  • azithromycin h influenzae March 5, 2018

    azithromycin h influenzae

    azithromycin h influenzae

  • escitalopram oxalate tab 10 mg March 5, 2018

    escitalopram oxalate tab 10 mg

    escitalopram oxalate tab 10 mg

  • viagra brand name in india March 5, 2018

    viagra brand name in india

    viagra brand name in india

  • what to avoid while taking cymbalta March 5, 2018

    what to avoid while taking cymbalta

    what to avoid while taking cymbalta

  • duloxetine and flexeril March 5, 2018

    duloxetine and flexeril

    duloxetine and flexeril

  • lexapro picture March 5, 2018

    lexapro picture

    lexapro picture

  • cephalexin cellulitis dose March 5, 2018

    cephalexin cellulitis dose

    cephalexin cellulitis dose

  • gabapentin breathing March 5, 2018

    gabapentin breathing

    gabapentin breathing

  • keflex treat sinus infection March 5, 2018

    keflex treat sinus infection

    keflex treat sinus infection

  • how many mg of fluoxetine for dogs March 5, 2018

    how many mg of fluoxetine for dogs

    how many mg of fluoxetine for dogs

  • metronidazole magyarul March 5, 2018

    metronidazole magyarul

    metronidazole magyarul

  • viibryd vs zoloft March 5, 2018

    viibryd vs zoloft

    viibryd vs zoloft

  • psy March 5, 2018

    psy

    psy

  • 911 March 5, 2018

    911

    911

  • losing weight with semaglutide March 5, 2018

    losing weight with semaglutide

    losing weight with semaglutide

  • 7mg rybelsus March 5, 2018

    7mg rybelsus

    7mg rybelsus

  • rybelsus success stories March 5, 2018

    rybelsus success stories

    rybelsus success stories

  • lisinopril winded March 5, 2018

    lisinopril winded

    lisinopril winded

  • furosemide refills March 5, 2018

    furosemide refills

    furosemide refills

  • metformin n3 March 5, 2018

    metformin n3

    metformin n3

  • nolvadex-d astrazeneca March 5, 2018

    nolvadex-d astrazeneca

    nolvadex-d astrazeneca

  • valtrex helps March 5, 2018

    valtrex helps

    valtrex helps

  • cyclobenzaprine and lyrica March 5, 2018

    cyclobenzaprine and lyrica

    cyclobenzaprine and lyrica

  • metronidazole свечи March 5, 2018

    metronidazole свечи

    metronidazole свечи

  • bactrim impotence March 5, 2018

    bactrim impotence

    bactrim impotence

  • gabapentin protein March 5, 2018

    gabapentin protein

    gabapentin protein

  • cialis 800 black canada March 5, 2018

    cialis 800 black canada

    cialis 800 black canada

  • cialis in melbourne australia March 5, 2018

    cialis in melbourne australia

    cialis in melbourne australia

  • best price for cialis 20 mg March 5, 2018

    best price for cialis 20 mg

    best price for cialis 20 mg

  • best price for cialis March 5, 2018

    best price for cialis

    best price for cialis

  • cheap viagra 100 March 5, 2018

    cheap viagra 100

    cheap viagra 100

  • real viagra from canada March 5, 2018

    real viagra from canada

    real viagra from canada

  • viagra 6800mg March 5, 2018

    viagra 6800mg

    viagra 6800mg

  • over the counter female viagra pill March 5, 2018

    over the counter female viagra pill

    over the counter female viagra pill

  • where to buy otc viagra March 5, 2018

    where to buy otc viagra

    where to buy otc viagra

  • female viagra pill March 5, 2018

    female viagra pill

    female viagra pill

  • neoral pharmacy March 5, 2018

    neoral pharmacy

    neoral pharmacy

  • how long does it take for tadalafil to work March 5, 2018

    how long does it take for tadalafil to work

    how long does it take for tadalafil to work

  • viagra pills cheap online March 5, 2018

    viagra pills cheap online

    viagra pills cheap online

  • cialis otc switch March 5, 2018

    cialis otc switch

    cialis otc switch

  • cialis for daily use cost March 5, 2018

    cialis for daily use cost

    cialis for daily use cost

  • generic women viagra March 5, 2018

    generic women viagra

    generic women viagra

  • cymbalta pharmacy checker March 5, 2018

    cymbalta pharmacy checker

    cymbalta pharmacy checker

  • giant food store pharmacy hours March 5, 2018

    giant food store pharmacy hours

    giant food store pharmacy hours

  • how long before sex should i take cialis March 5, 2018

    how long before sex should i take cialis

    how long before sex should i take cialis

  • cialis egypt March 5, 2018

    cialis egypt

    cialis egypt

  • cialis tadalafil 20mg preis March 5, 2018

    cialis tadalafil 20mg preis

    cialis tadalafil 20mg preis