diff --git a/README.md b/README.md index 4b6e260..14163c4 100644 --- a/README.md +++ b/README.md @@ -135,18 +135,22 @@ _Внимание_! Только Python 2.7, только PostgreSQL, тольк ## Настройка ### Первоначальная настройка базы данных 1. Настроим конфиг, он лежит в `aore/config/__init__.py`, в этом файле можно изменить `.dev` на `.prod`, - отредактировать, соотвественно, dev.py или prod.py: прописать параметры доступа к базе и путь, - куда будут сохраняться данные Sphinx; по этому пути дополнительно необходимо создать 3 папки: log, run и data +отредактировать, соотвественно, dev.py или prod.py: прописать параметры доступа к базе и путь, +куда будут сохраняться данные Sphinx; по этому пути дополнительно необходимо создать 3 папки: log, run и data + 2. Создадим базу: - `sudo -u phias python manage.py -b create -s /tmp/fias_xml.rar` - из архива. - `sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked` - из директории. - `sudo -u phias python manage.py -b create -s http` - онлайн, с сервера ФНС. - Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version `, где num - - номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v` +- из архива `sudo -u phias python manage.py -b create -s /tmp/fias_xml.rar` +- из директории `sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked` +- онлайн, с сервера ФНС `sudo -u phias python manage.py -b create -s http` + Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version `, где num - +номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`. + 3. Проиндексируем Sphinx: - Windows: `python manage.py -c -i C://sphinx//indexer.exe -o C://sphinx//sphinx.conf` - Debian: `sudo python manage.py -c -i indexer -o /usr/local/sphinx/etc/sphinx.conf` +- Windows: `python manage.py -c -i C://sphinx//indexer.exe -o C://sphinx//sphinx.conf` +- Debian: `sudo python manage.py -c -i indexer -o /usr/local/sphinx/etc/sphinx.conf` + 4. Затем запустим searchd: - Windows: `net start sphinxsearch`, при этом файл настройки должен быть доступен Sphinx'у. - Debian: `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf` +- Windows: `net start sphinxsearch`, при этом файл настройки должен быть доступен Sphinx'у. +- Debian: `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf` + 5. Настроим WSGI server, я использую nginx + passenger, Вы можете использовать любое приемлемое сочетание. \ No newline at end of file diff --git a/aore/fias/search.py b/aore/fias/search.py index 9660047..bebde08 100644 --- a/aore/fias/search.py +++ b/aore/fias/search.py @@ -1,31 +1,27 @@ # -*- coding: utf-8 -*- import re +import time import Levenshtein import sphinxapi -import time +from aore.config import basic from aore.config import sphinx_conf from aore.fias.wordentry import WordEntry +from aore.fias.wordvariation import VariationType from aore.miscutils.trigram import trigram -from aore.config import basic class SphinxSearch: # Config's delta_len = 2 - rating_limit_soft = 0.41 - rating_limit_soft_count = 6 - - rating_limit_hard = 0.82 - rating_limit_hard_count = 3 - default_rating_delta = 2 regression_coef = 0.08 max_result = 10 - exclude_freq_words = True + # Конфиги, которые в будущем, возможно, будут настраиваемы пользователем (как strong) + search_freq_words = True def __init__(self, db): self.db = db @@ -39,16 +35,17 @@ class SphinxSearch: self.client_show.SetLimits(0, self.max_result) self.client_show.SetConnectTimeout(3.0) - def __configure(self, index_name, wlen=None): + def __configure(self, index_name, word_len=None): self.client_sugg.ResetFilters() - if index_name == sphinx_conf.index_sugg and wlen: + if index_name == sphinx_conf.index_sugg and word_len: self.client_sugg.SetRankingMode(sphinxapi.SPH_RANK_WORDCOUNT) - self.client_sugg.SetFilterRange("len", int(wlen) - self.delta_len, int(wlen) + self.delta_len) - self.client_sugg.SetSelect("word, len, @weight+{}-abs(len-{}) AS krank".format(self.delta_len, wlen)) + self.client_sugg.SetFilterRange("len", int(word_len) - self.delta_len, int(word_len) + self.delta_len) + self.client_sugg.SetSelect("word, len, @weight+{}-abs(len-{}) AS krank".format(self.delta_len, word_len)) self.client_sugg.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC") else: self.client_show.SetRankingMode(sphinxapi.SPH_RANK_BM25) - #self.client_show.SetSortMode(sphinxapi.SPH_SORT_) + #self.client_show.SetSelect("aoid, fullname, @weight AS krank") + #self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC") def __get_suggest(self, word, rating_limit, count): word_len = str(len(word) / 2) @@ -83,35 +80,12 @@ class SphinxSearch: return outlist - def __add_word_variations(self, word_entry, strong): - if word_entry.MT_MANY_SUGG and not strong: - suggs = self.__get_suggest(word_entry.word, self.rating_limit_soft, self.rating_limit_soft_count) - for suggestion in suggs: - word_entry.add_variation(suggestion[0]) - if word_entry.MT_SOME_SUGG and not strong: - suggs = self.__get_suggest(word_entry.word, self.rating_limit_hard, self.rating_limit_hard_count) - for suggestion in suggs: - word_entry.add_variation(suggestion[0]) - if word_entry.MT_LAST_STAR: - word_entry.add_variation(word_entry.word + '*') - if word_entry.MT_AS_IS: - word_entry.add_variation(word_entry.word) - if word_entry.MT_ADD_SOCR: - word_entry.add_variation_socr() - - # Получает список объектов (слово), пропуская часто используемые слова + # Получает список объектов (слово) def __get_word_entries(self, words, strong): we_list = [] for word in words: if word != '': - we = WordEntry(self.db, word) - if self.exclude_freq_words and we.is_freq_word: - pass - else: - self.__add_word_variations(we, strong) - - assert we.get_variations() != "", "Cannot process sentence." - we_list.append(we) + we_list.append(WordEntry(self.db, word)) return we_list @@ -123,19 +97,44 @@ class SphinxSearch: # сплитим текст на слова words = split_phrase(text) - # получаем список объектов + # получаем список объектов (слов) word_entries = self.__get_word_entries(words, strong) word_count = len(word_entries) # проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое) assert word_count > 0, "No legal words is specified" - # формируем строки для поиска в Сфинксе - for x in range(word_count, max(0, word_count - 3), -1): - self.client_show.AddQuery("\"{}\"/{} \"ул \"/0".format(" ".join(x.get_variations() for x in word_entries), x), - sphinx_conf.index_addjobj) + # получаем все вариации слов + all_variations = [] + for we in word_entries: + for vari in we.variations_gen(strong, self.__get_suggest): + all_variations.append(vari) - self.__configure(sphinx_conf.index_addjobj) + good_vars = [v for v in all_variations if v.var_type == VariationType.normal] + freq_vars = [v for v in all_variations if v.var_type == VariationType.freq] + + good_vars_word_count = len(set([v.parent for v in good_vars])) + freq_vars_word_count = len(set([v.parent for v in freq_vars])) + + # формируем строки для поиска в Сфинксе + for i in range(good_vars_word_count, max(0, good_vars_word_count - 3), -1): + first_q = "\"{}\"/{}".format(" ".join(good_var.text for good_var in good_vars), i) + + if self.search_freq_words: + for j in range(freq_vars_word_count, -1, -1): + if j == 0: + second_q = "" + else: + second_q = " \"{}\"/{}".format(" ".join(freq_var.text for freq_var in freq_vars), j) + second_q = second_q.replace("*", "") + + print first_q + second_q + self.client_show.AddQuery(first_q + second_q, sphinx_conf.index_addjobj) + else: + print first_q + self.client_show.AddQuery(first_q, sphinx_conf.index_addjobj) + + self.__configure(sphinx_conf.index_addjobj, word_count) start_t = time.time() rs = self.client_show.RunQueries() @@ -154,8 +153,9 @@ class SphinxSearch: if not ma['attrs']['aoid'] in parsed_ids: parsed_ids.append(ma['attrs']['aoid']) results.append( - dict(aoid=ma['attrs']['aoid'], text=unicode(ma['attrs']['fullname']), ratio=ma['weight'], cort=i)) + dict(aoid=ma['attrs']['aoid'], text=unicode(ma['attrs']['fullname']), ratio=ma['weight'], + cort=i)) - # results.sort(key=lambda x: Levenshtein.ratio(text, x['text']), reverse=True) + # results.sort(key=lambda x: Levenshtein.ratio(text, x['text']), reverse=False) return results diff --git a/aore/fias/wordentry.py b/aore/fias/wordentry.py index 2e34047..bea200d 100644 --- a/aore/fias/wordentry.py +++ b/aore/fias/wordentry.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import re -from aore.config import basic from aore.config import sphinx_conf +from aore.fias.wordvariation import WordVariation, VariationType class WordEntry: @@ -42,42 +42,68 @@ class WordEntry: MT_ADD_SOCR=['..10', '..x0'] ) + rating_limit_soft = 0.41 + rating_limit_soft_count = 6 + + rating_limit_hard = 0.82 + rating_limit_hard_count = 3 + def __init__(self, db, word): self.db = db self.word = str(word) self.word_len = len(unicode(self.word)) - self.variations = [] - self.scname = None - self.is_freq_word = False - self.ranks = self.__get_ranks() + self.parameters = dict(IS_FREQ=False, SOCR_WORD=None) + self.ranks = self.__init_ranks() - for x, y in self.match_types.iteritems(): - self.__dict__[x] = False - for z in y: - self.__dict__[x] = self.__dict__[x] or re.search(z, self.ranks) is not None + # Заполняем параметры слова + for mt_name, mt_values in self.match_types.iteritems(): + self.__dict__[mt_name] = False + for mt_value in mt_values: + self.__dict__[mt_name] = self.__dict__[mt_name] or re.search(mt_value, self.ranks) is not None # Если ищем по лайку, то точное совпадение не ищем (оно и так будет включено) if self.MT_LAST_STAR: self.MT_AS_IS = False - # Строка слишком котроткая, то по лайку не ищем, будет очень долго - if self.MT_LAST_STAR and self.word_len < sphinx_conf.min_length_to_star: + # Строка слишком котроткая, то по лайку не ищем, сфинкс такого не прожует + # Если найдено сокращение, то по лайку тоже не ищем TODO добавить это в правила + if self.MT_LAST_STAR and (self.word_len < sphinx_conf.min_length_to_star or self.MT_ADD_SOCR): self.MT_LAST_STAR = False self.MT_AS_IS = True - def add_variation_socr(self): - if self.scname: - self.add_variation(self.scname) + def variations_gen(self, strong, suggestion_func): + default_var_type = VariationType.normal + # Если слово встречается часто, ставим у всех вариантов тип VariationType.freq + if self.parameters['IS_FREQ']: + default_var_type = VariationType.freq - def add_variation(self, variation_string): - self.variations.append(variation_string) + # Добавляем подсказки (много штук) + if self.MT_MANY_SUGG and not strong: + suggs = suggestion_func(self.word, self.rating_limit_soft, self.rating_limit_soft_count) + for suggestion in suggs: + yield WordVariation(self, suggestion[0], default_var_type) - def get_variations(self): - if len(self.variations) == 1: - return self.variations[0] - return "{}".format(" ".join(self.variations)) + # Добавляем подсказки (немного) + if self.MT_SOME_SUGG and not strong: + suggs = suggestion_func(self.word, self.rating_limit_hard, self.rating_limit_hard_count) + for suggestion in suggs: + yield WordVariation(self, suggestion[0], default_var_type) - def __get_ranks(self): + # Добавляем звездочку на конце + if self.MT_LAST_STAR: + yield WordVariation(self, self.word + '*', default_var_type) + + # Добавляем слово "как есть" + if self.MT_AS_IS: + yield WordVariation(self, self.word, default_var_type) + + # -- Дополнительные функции -- + # Добавляем сокращение + if self.MT_ADD_SOCR: + if self.parameters['SOCR_WORD']: + yield WordVariation(self, self.parameters['SOCR_WORD'], VariationType.freq) + + def __init_ranks(self): sql_qry = "SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word LIKE '{}%' AND LENGTH(word) > {} " \ "UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \ "UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \ @@ -88,12 +114,12 @@ class WordEntry: result = self.db.get_rows(sql_qry) # Проставляем "сокращенное" сокращение, если нашли полное - if not self.scname: - self.scname = result[2][1] + if not self.parameters['SOCR_WORD']: + self.parameters['SOCR_WORD'] = result[2][1] # Проверяем, если слово встречается слишком много раз if len(result) == 5 and result[4][0] > 30000: - self.is_freq_word = True + self.parameters['IS_FREQ'] = True # Формируем список найденных величин совпадений: # result[x] @@ -118,4 +144,3 @@ class WordEntry: def __str__(self): return str(self.word) - diff --git a/aore/fias/wordvariation.py b/aore/fias/wordvariation.py new file mode 100644 index 0000000..70a9e41 --- /dev/null +++ b/aore/fias/wordvariation.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from enum import Enum # Типы вариаций слова + + +class VariationType(Enum): + normal = 0 + freq = 1 + + +class WordVariation: + def __init__(self, parent_word, text, var_type=VariationType.normal): + self.parent = parent_word + self.text = text + self.var_type = var_type diff --git a/aore/templates/postgre/sphinx_query.sql b/aore/templates/postgre/sphinx_query.sql index ea4e93d..00b0780 100644 --- a/aore/templates/postgre/sphinx_query.sql +++ b/aore/templates/postgre/sphinx_query.sql @@ -9,6 +9,6 @@ PATH.fullname || ', ' || child.shortname || ' ' || child.formalname AS fullname FROM "ADDROBJ" AS child , PATH - WHERE child.parentguid = PATH.aoguid + WHERE child.parentguid = PATH.aoguid AND actstatus = TRUE AND livestatus = TRUE AND nextid IS NULL ) - SELECT * FROM PATH WHERE AOLEVEL NOT IN (1,3) \ No newline at end of file + SELECT p.cnt, p.aoid, p.fullname, length(p.fullname)-length(replace(p.fullname, ' ', '')) as wordcount FROM PATH p WHERE p.AOLEVEL NOT IN (1, 3) \ No newline at end of file diff --git a/aore/templates/sphinx/idx_addrobj.conf b/aore/templates/sphinx/idx_addrobj.conf index 1ce634d..d8b10d2 100644 --- a/aore/templates/sphinx/idx_addrobj.conf +++ b/aore/templates/sphinx/idx_addrobj.conf @@ -10,9 +10,9 @@ source {{index_name}} sql_query = {{!sql_query}} sql_field_string = fullname + sql_attr_uint = len sql_attr_string = aoid sql_attr_string = aoguid - sql_attr_uint = aolevel } index {{ index_name }}