class BasicSearch: def __init__(self, snippet_limit: int = 3, take_words: int = 3, results_limit: int = 10, data_dir: str = 'pages') -> None: """ This class is used for basic, no SQL searching. :param take_words: How many words to display is snippets before/after the found word. :param results_limit: How many rows to show when displaying top results with snippets. :param data_dir: Directory in which the HTML sites are saved relative to the current directory. """ # nltk.download('punkt') # nltk.download('stopwords') self.time_needed_to_search = None self.take_words = take_words self.results_limit = results_limit self.data_dir = os.path.join(os.getcwd(), data_dir) self.path = data_dir self.snippet_limit = snippet_limit self.snip = Snippet(self.take_words, self.results_limit) self.stop_words_slovene = set(stopwords.words("Slovene")).union({ "ter", "nov", "novo", "nova", "zato", "še", "zaradi", "a", "ali", "april", "avgust", "b", "bi", "bil", "bila", "bile", "bili", "bilo", "biti", "blizu", "bo", "bodo", "bojo", "bolj", "bom", "bomo", "boste", "bova", "boš", "brez", "c", "cel", "cela", "celi", "celo", "d", "da", "daleč", "dan", "danes", "datum", "december", "deset", "deseta", "deseti", "deseto", "devet", "deveta", "deveti", "deveto", "do", "dober", "dobra", "dobri", "dobro", "dokler", "dol", "dolg", "dolga", "dolgi", "dovolj", "drug", "druga", "drugi", "drugo", "dva", "dve", "e", "eden", "en", "ena", "ene", "eni", "enkrat", "eno", "etc.", "f", "februar", "g", "g.", "ga", "ga.", "gor", "gospa", "gospod", "h", "halo", "i", "idr.", "ii", "iii", "in", "iv", "ix", "iz", "j", "januar", "jaz", "je", "ji", "jih", "jim", "jo", "julij", "junij", "jutri", "k", "kadarkoli", "kaj", "kajti", "kako", "kakor", "kamor", "kamorkoli", "kar", "karkoli", "katerikoli", "kdaj", "kdo", "kdorkoli", "ker", "ki", "kje", "kjer", "kjerkoli", "ko", "koder", "koderkoli", "koga", "komu", "kot", "kratek", "kratka", "kratke", "kratki", "l", "lahka", "lahke", "lahki", "lahko", "le", "lep", "lepa", "lepe", "lepi", "lepo", "leto", "m", "maj", "majhen", "majhna", "majhni", "malce", "malo", "manj", "marec", "me", "med", "medtem", "mene", "mesec", "mi", "midva", "midve", "mnogo", "moj", "moja", "moje", "mora", "morajo", "moram", "moramo", "morate", "moraš", "morem", "mu", "n", "na", "nad", "naj", "najina", "najino", "najmanj", "naju", "največ", "nam", "narobe", "nas", "nato", "nazaj", "naš", "naša", "naše", "ne", "nedavno", "nedelja", "nek", "neka", "nekaj", "nekatere", "nekateri", "nekatero", "nekdo", "neke", "nekega", "neki", "nekje", "neko", "nekoga", "nekoč", "ni", "nikamor", "nikdar", "nikjer", "nikoli", "nič", "nje", "njega", "njegov", "njegova", "njegovo", "njej", "njemu", "njen", "njena", "njeno", "nji", "njih", "njihov", "njihova", "njihovo", "njiju", "njim", "njo", "njun", "njuna", "njuno", "no", "nocoj", "november", "npr.", "o", "ob", "oba", "obe", "oboje", "od", "odprt", "odprta", "odprti", "okoli", "oktober", "on", "onadva", "one", "oni", "onidve", "osem", "osma", "osmi", "osmo", "oz.", "p", "pa", "pet", "peta", "petek", "peti", "peto", "po", "pod", "pogosto", "poleg", "poln", "polna", "polni", "polno", "ponavadi", "ponedeljek", "ponovno", "potem", "povsod", "pozdravljen", "pozdravljeni", "prav", "prava", "prave", "pravi", "pravo", "prazen", "prazna", "prazno", "prbl.", "precej", "pred", "prej", "preko", "pri", "pribl.", "približno", "primer", "pripravljen", "pripravljena", "pripravljeni", "proti", "prva", "prvi", "prvo", "r", "ravno", "redko", "res", "reč", "s", "saj", "sam", "sama", "same", "sami", "samo", "se", "sebe", "sebi", "sedaj", "sedem", "sedma", "sedmi", "sedmo", "sem", "september", "seveda", "si", "sicer", "skoraj", "skozi", "slab", "smo", "so", "sobota", "spet", "sreda", "srednja", "srednji", "sta", "ste", "stran", "stvar", "sva", "t", "ta", "tak", "taka", "take", "taki", "tako", "takoj", "tam", "te", "tebe", "tebi", "tega", "težak", "težka", "težki", "težko", "ti", "tista", "tiste", "tisti", "tisto", "tj.", "tja", "to", "toda", "torek", "tretja", "tretje", "tretji", "tri", "tu", "tudi", "tukaj", "tvoj", "tvoja", "tvoje", "u", "v", "vaju", "vam", "vas", "vaš", "vaša", "vaše", "ve", "vedno", "velik", "velika", "veliki", "veliko", "vendar", "ves", "več", "vi", "vidva", "vii", "viii", "visok", "visoka", "visoke", "visoki", "vsa", "vsaj", "vsak", "vsaka", "vsakdo", "vsake", "vsaki", "vsakomur", "vse", "vsega", "vsi", "vso", "včasih", "včeraj", "x", "z", "za", "zadaj", "zadnji", "zakaj", "zaprta", "zaprti", "zaprto", "zdaj", "zelo", "zunaj", "č", "če", "često", "četrta", "četrtek", "četrti", "četrto", "čez", "čigav", "š", "šest", "šesta", "šesti", "šesto", "štiri", "ž", "že", "svoj", "jesti", "imeti", "\u0161e", "iti", "kak", "www", "km", "eur", "pač", "del", "kljub", "šele", "prek", "preko", "znova", "morda", "kateri", "katero", "katera", "ampak", "lahek", "lahka", "lahko", "morati", "torej", "gl", "xsd", "ipd", "om", "gt", "lt", "d.o.o" }) def print_results(self, query: list, results: list) -> None: """ Prints the results of the query to the std output :param query: The user search query in list format. :param results: A list of lists containing the results in format [frequencies, document, snippets]. The list should be ordered in descending order. """ print(f'Results for query: "{" ".join(query)}"') print('{} results found in {:.0f}s'.format(len(results), self.time_needed_to_search)) print("{:<12} {:<40} {}".format('Frequencies', 'Document', 'Snippets')) print("{} {} {}".format('-' * 12, '-' * 40, '-' * 80)) for i in range(min(self.results_limit, len(results))): print("{:<12} {:<40} {}".format( results[i][0], results[i][1], '... ' + ' ... '.join(results[i][2][:self.snippet_limit]) + ' ...')) def get_files(self) -> list: """ Gets a list of paths to all HTML files which are contained in the self.path folder. :return: List of all HTML files. """ files_path = [ os.path.join(dp, f) for dp, dn, filenames in os.walk(self.path) for f in filenames if os.path.splitext(f)[1].lower() == '.html' ] return files_path def get_document(self, page_name: str) -> object: """ Gets and opens the provided document. :param page_name: The HTML document that should be opened for reading. :return: The opened document. """ map_name = '.'.join(page_name.split('.')[:3]) return open(os.path.join(self.data_dir, "{}/{}".format(map_name, page_name)), 'r', encoding='utf-8') def parse_file(self, file: str, search_words: list) -> tuple: """ Searches the HTML document for the words in the query and returns the frequency and snippets. :param file: The HTML file to search. :param search_words: The user search query. :return: A touple with frequencies and result snippets. """ # open the file f = codecs.open(file, 'r', 'utf-8') soup = BeautifulSoup(f.read(), features="html.parser") for script in soup(["script", "style"]): script.decompose() # tokenize and clean file strips = list(soup.stripped_strings) document = ' '.join(strips) tokens = word_tokenize(document, language='Slovene', preserve_line=False) indexes = list() for i in range(len(tokens)): token = tokens[i].lower().replace("'", "").replace("'", '').replace( '`', '').replace('·', '') # skip if token doesn't contain letters if not re.search('[a-žA-ž]', token): continue # skip tokens of length 1, if they're not a word or number if len(token) == 1 and not re.match("^[A-Ža-ž0-9]*$", token): continue # if token ends with a special character, remove it if len(token) >= 2 and not re.match("^[A-Ža-ž0-9]*$", token[-1]): token = token[:-1] # if token starts with a special character, remove it if len(token) >= 2 and not re.match("^[A-Ža-ž0-9]*$", token[0]): token = token[1:] # if token is a stop-word, continue if token not in self.stop_words_slovene: # if we're searching for this token, add the index to the list if token in search_words: indexes.append(i) try: # snippet = self.get_snip(tokens, indexes) snippet = self.snip.get_snip(tokens, indexes) except AssertionError: snippet = [] return len(indexes), snippet def search(self, words: list) -> None: """ Performs the search in all files for the user query. :param words: The user search query. """ results = [] # start the timer and do the search, after that calculate the time needed for search start_time = time.time() files = self.get_files() for file in files: f, s = self.parse_file(file, words) if f > 0: results.append([f, ntpath.split(file)[1], s]) end_time = time.time() self.time_needed_to_search = end_time - start_time results.sort(key=lambda x: x[0], reverse=True) self.print_results(words, results)
class SQLiteSearch: def __init__(self, snippet_limit=3, take_words=3, results_limit=10, db_name='inverted-index.db', data_dir='pages'): self.time_needed_to_search = None self.db_name = db_name self.take_words = take_words self.snippet_limit = snippet_limit self.results_limit = results_limit self.data_dir = os.path.join(os.getcwd(), data_dir) self.all_results = None self.snip = Snippet(self.take_words, self.results_limit) self.stop_words_slovene = set( nltk.corpus.stopwords.words("Slovene") ).union({ "ter", "nov", "novo", "nova", "zato", "še", "zaradi", "a", "ali", "april", "avgust", "b", "bi", "bil", "bila", "bile", "bili", "bilo", "biti", "blizu", "bo", "bodo", "bojo", "bolj", "bom", "bomo", "boste", "bova", "boš", "brez", "c", "cel", "cela", "celi", "celo", "d", "da", "daleč", "dan", "danes", "datum", "december", "deset", "deseta", "deseti", "deseto", "devet", "deveta", "deveti", "deveto", "do", "dober", "dobra", "dobri", "dobro", "dokler", "dol", "dolg", "dolga", "dolgi", "dovolj", "drug", "druga", "drugi", "drugo", "dva", "dve", "e", "eden", "en", "ena", "ene", "eni", "enkrat", "eno", "etc.", "f", "februar", "g", "g.", "ga", "ga.", "gor", "gospa", "gospod", "h", "halo", "i", "idr.", "ii", "iii", "in", "iv", "ix", "iz", "j", "januar", "jaz", "je", "ji", "jih", "jim", "jo", "julij", "junij", "jutri", "k", "kadarkoli", "kaj", "kajti", "kako", "kakor", "kamor", "kamorkoli", "kar", "karkoli", "katerikoli", "kdaj", "kdo", "kdorkoli", "ker", "ki", "kje", "kjer", "kjerkoli", "ko", "koder", "koderkoli", "koga", "komu", "kot", "kratek", "kratka", "kratke", "kratki", "l", "lahka", "lahke", "lahki", "lahko", "le", "lep", "lepa", "lepe", "lepi", "lepo", "leto", "m", "maj", "majhen", "majhna", "majhni", "malce", "malo", "manj", "marec", "me", "med", "medtem", "mene", "mesec", "mi", "midva", "midve", "mnogo", "moj", "moja", "moje", "mora", "morajo", "moram", "moramo", "morate", "moraš", "morem", "mu", "n", "na", "nad", "naj", "najina", "najino", "najmanj", "naju", "največ", "nam", "narobe", "nas", "nato", "nazaj", "naš", "naša", "naše", "ne", "nedavno", "nedelja", "nek", "neka", "nekaj", "nekatere", "nekateri", "nekatero", "nekdo", "neke", "nekega", "neki", "nekje", "neko", "nekoga", "nekoč", "ni", "nikamor", "nikdar", "nikjer", "nikoli", "nič", "nje", "njega", "njegov", "njegova", "njegovo", "njej", "njemu", "njen", "njena", "njeno", "nji", "njih", "njihov", "njihova", "njihovo", "njiju", "njim", "njo", "njun", "njuna", "njuno", "no", "nocoj", "november", "npr.", "o", "ob", "oba", "obe", "oboje", "od", "odprt", "odprta", "odprti", "okoli", "oktober", "on", "onadva", "one", "oni", "onidve", "osem", "osma", "osmi", "osmo", "oz.", "p", "pa", "pet", "peta", "petek", "peti", "peto", "po", "pod", "pogosto", "poleg", "poln", "polna", "polni", "polno", "ponavadi", "ponedeljek", "ponovno", "potem", "povsod", "pozdravljen", "pozdravljeni", "prav", "prava", "prave", "pravi", "pravo", "prazen", "prazna", "prazno", "prbl.", "precej", "pred", "prej", "preko", "pri", "pribl.", "približno", "primer", "pripravljen", "pripravljena", "pripravljeni", "proti", "prva", "prvi", "prvo", "r", "ravno", "redko", "res", "reč", "s", "saj", "sam", "sama", "same", "sami", "samo", "se", "sebe", "sebi", "sedaj", "sedem", "sedma", "sedmi", "sedmo", "sem", "september", "seveda", "si", "sicer", "skoraj", "skozi", "slab", "smo", "so", "sobota", "spet", "sreda", "srednja", "srednji", "sta", "ste", "stran", "stvar", "sva", "t", "ta", "tak", "taka", "take", "taki", "tako", "takoj", "tam", "te", "tebe", "tebi", "tega", "težak", "težka", "težki", "težko", "ti", "tista", "tiste", "tisti", "tisto", "tj.", "tja", "to", "toda", "torek", "tretja", "tretje", "tretji", "tri", "tu", "tudi", "tukaj", "tvoj", "tvoja", "tvoje", "u", "v", "vaju", "vam", "vas", "vaš", "vaša", "vaše", "ve", "vedno", "velik", "velika", "veliki", "veliko", "vendar", "ves", "več", "vi", "vidva", "vii", "viii", "visok", "visoka", "visoke", "visoki", "vsa", "vsaj", "vsak", "vsaka", "vsakdo", "vsake", "vsaki", "vsakomur", "vse", "vsega", "vsi", "vso", "včasih", "včeraj", "x", "z", "za", "zadaj", "zadnji", "zakaj", "zaprta", "zaprti", "zaprto", "zdaj", "zelo", "zunaj", "č", "če", "često", "četrta", "četrtek", "četrti", "četrto", "čez", "čigav", "š", "šest", "šesta", "šesti", "šesto", "štiri", "ž", "že", "svoj", "jesti", "imeti", "\u0161e", "iti", "kak", "www", "km", "eur", "pač", "del", "kljub", "šele", "prek", "preko", "znova", "morda", "kateri", "katero", "katera", "ampak", "lahek", "lahka", "lahko", "morati", "torej", "gl", "xsd", "ipd", "om", "gt", "lt", "d.o.o" }) self.conn = sqlite3.connect(self.db_name) def build_query(self, words): # -3 because we want to remove the last or and space (so 3 chars) from the list rm = len('or ') * -1 if not isinstance(words, list): words = [words] whr = ' '.join(['word = \'{}\' or'.format(w) for w in words])[:rm] qry = "select documentName as file, SUM(frequency) 'freq', GROUP_CONCAT(indexes) as idx " \ "from posting " \ "where {} " \ "group by documentName " \ "order by freq desc ".format(whr) return qry @staticmethod def process_results(results): if len(results): return [[ result[0], result[1], [int(i) for i in result[2].split(',')] ] for result in results] def print_results(self, query, frequencies, pages, snippets): print(f'Results for query: "{" ".join(query)}"') print('{} results found in {:.0f}ms'.format( self.all_results, self.time_needed_to_search)) print("{:<12} {:<40} {}".format('Frequencies', 'Document', 'Snippets')) print("{} {} {}".format('-' * 12, '-' * 40, '-' * 80)) for i in range(min(self.results_limit, len(pages))): print("{:<12} {:<40} {}".format( frequencies[i], pages[i], '... ' + ' ... '.join(snippets[i][:self.snippet_limit]) + ' ...')) def search_db(self, words): cursor = self.conn.cursor() query = self.build_query(words) cursor.execute(query) results = cursor.fetchall() return self.process_results(results) def get_document(self, page_name): map_name = '.'.join(page_name.split('.')[:3]) return open(os.path.join(self.data_dir, "{}/{}".format(map_name, page_name)), 'r', encoding='utf-8') def search(self, words): # start the timer and do the search, after that calculate the time needed for search start_time = time.time() results = self.search_db(words) self.all_results = len(results) end_time = time.time() self.time_needed_to_search = (end_time - start_time) * 1000 pages, frequencies, snippets = [], [], [] ctr = 0 for page_file, frequency, indexes in results: if ctr > self.results_limit: break ctr += 1 pages.append(page_file) frequencies.append(frequency) document = self.get_document(page_file) soup = BeautifulSoup(document, features="html.parser") for script in soup(["script", "style"]): script.decompose() strips = list(soup.stripped_strings) text = ' '.join(strips) tokens = nltk.word_tokenize(text, language='Slovene', preserve_line=False) word_tokens = [] for i in range(len(tokens)): w = tokens[i].lower().replace("'", "").replace("'", '').replace( '`', '').replace('·', '') if not re.search('[a-žA-ž]', w): continue if len(w) == 1 and not re.match("^[A-Ža-ž0-9]*$", w): continue if len(w) >= 2 and not re.match("^[A-Ža-ž0-9]*$", w[-1]): w = w[:-1] if len(w) >= 2 and not re.match("^[A-Ža-ž0-9]*$", w[0]): w = w[1:] if w not in self.stop_words_slovene: word_tokens.append(w) # snippets.append(self.get_snip(tokens, indexes)) snippets.append(self.snip.get_snip(tokens, indexes)) self.print_results(words, frequencies, pages, snippets)