retriever.py (5322B)
1 import gaoya 2 import pandas as pd 3 import sys 4 from fire import Fire 5 import zstandard 6 from time import time 7 8 9 class Retriever: 10 def _preprocess(self, stext): 11 # nonletters = r'[^a-zA-Z]' 12 nonletters = r'[_0-9\W]' 13 return stext.str.replace(nonletters, '', regex=True) 14 15 16 def preprocess_content(self, fcontent='easy.jsonl'): 17 """ 18 Preprocess content for faster adding to the index. 19 To avoid repeated ids, we don't use them, instead use line numbers. 20 """ 21 print('Loading', fcontent) 22 df = pd.read_json(fcontent, lines=True) 23 #df = df.set_index('id') # There are duplicates in full.jsonl! 24 df.index += 1 # line numbers starting with 1 to avoid +0=-0 problem 25 print('Preprocessing', fcontent) 26 s1 = self._preprocess(df.article) 27 s2 = self._preprocess(df.headline + ' ' + df.article) 28 s2.index *= -1 # negated line numbers correspond to concat(headline,article) 29 text_ser = pd.concat([s1, s2]) 30 print('Dumping to disk') 31 text_ser.to_csv(fcontent+'.preproc.tsv', sep='\t') 32 text_ser.to_pickle(fcontent+'.preproc.pkl') 33 34 print('Survived chars:') 35 survived_chars = set.union(*text_ser.apply(set)) 36 print(repr(''.join(sorted(survived_chars)))) 37 print('Removed chars:') 38 all_chars = set.union(*df.article.apply(set)) 39 print(repr(''.join(sorted(all_chars - survived_chars)))) 40 41 42 def __init__(self, fcontent=None, ng_min=10, ng_max=10): 43 if fcontent is None: 44 return 45 # import pdb 46 # pdb.set_trace() 47 text_ser = pd.read_pickle(fcontent) 48 n = len(text_ser) 49 text_ser.drop(text_ser[text_ser.str.len() < ng_min].index, inplace=True) 50 print(f'{n} documents loaded, {(n-len(text_ser))/n*100}% were dropped because they are shorter than ng_min={ng_min} chars', file=sys.stderr) 51 52 index = gaoya.minhash.MinHashStringIndex(hash_size=32, 53 jaccard_threshold=0.5, 54 num_bands=42, 55 band_size=3, 56 analyzer='char', 57 lowercase=True, 58 ngram_range=(ng_min,ng_max)) 59 60 index.par_bulk_insert_docs(text_ser.index, text_ser) 61 print(f'{len(text_ser)} documents added to index', file=sys.stderr) 62 del text_ser 63 self.index = index 64 self.ng_min = ng_min 65 66 67 def _retrieve(self, qdf, out=sys.stdout): 68 """ 69 qdf: a DataFrame with 2 columns: 'qid' and 'text' 70 """ 71 if len(qdf) == 0: return 72 qser = self._preprocess(qdf.text) 73 short_mask = qdf[qser.str.len() < self.ng_min].index 74 qser.drop(short_mask, inplace=True) 75 qdf.drop(short_mask, inplace=True) 76 res = self.index.par_bulk_query(qser, return_similarity=True) 77 if not any(r for r in res): 78 return # optimization fot the case of no matches 79 qdf['ans'] = res 80 qdf = qdf.explode(column='ans', ignore_index=True) # lists of answers to 1 answer per row 81 qdf.dropna(subset='ans', inplace=True) # empty lists of answers 82 qdf['tid'], qdf['sim'] = qdf.ans.str[0], qdf.ans.str[1] 83 if len(qdf) > 0: 84 qdf[['qid','tid','sim','text']].to_json(out, orient='records',lines=True) 85 86 87 def retrieve(self, finp='-', batch_size=10000, text_field='text'): 88 """ 89 Retrieve near-duplicated of texts from stdin (by default) or file. 90 For each query text finds similar texts in the index, prints (qid, tid, sim, text) to stdout, where 91 qid and tid are query text and retrieved text identifiers, sim is their Jaccard similarity, text is the 92 retrieved text. 93 NB: It is recommended to use batches: 1K queries => 102ms, 10K queries => 955 ms 94 NB: texts containing less than ng_min letters are not indexed or queried because they become an empty set of ngrams 95 """ 96 start_line_num = 1 97 with sys.stdin if finp=='-' else zstandard.open(finp,'r') as inp: 98 while True: 99 st = time() 100 df = pd.read_json(inp, nrows=batch_size, lines=True, orient='records', dtype=False) # dtype=False loads text correctly, otherwise tries infers float/int types sometimes 101 if len(df)==0: break # EOF 102 if text_field != 'text': 103 df.rename(columns={text_field:'text'}, inplace=True) 104 if finp != '-': 105 df['qid'] = f'{finp}:' + (df.index + start_line_num).astype(str) 106 start_line_num += len(df) 107 else: 108 df = df[['index','text']].rename(columns={'index':'qid'}) 109 df.dropna(subset='text', inplace=True) 110 dur1 = time() - st 111 st = time() 112 self._retrieve(df) 113 dur2 = time() - st 114 print(f"{dur1+dur2}s per batch of {batch_size} docs, {len(df)} queries after filtering, {dur1/dur2}x longer reading than searching", file=sys.stderr) 115 116 117 118 Fire(Retriever)