Refactor. Deleted setup.py

This commit is contained in:
Jack Stdin
2016-02-23 22:29:12 +03:00
parent dc8e5d35a7
commit 86426d4803
8 changed files with 16 additions and 33 deletions

0
aore/search/__init__.py Normal file
View File

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
import re
import urllib
from uuid import UUID
import psycopg2
from bottle import template
from aore.config import db_conf
from aore.dbutils.dbimpl import DBImpl
from aore.search.search import SphinxSearch
class FiasFactory:
def __init__(self):
self.db = DBImpl(psycopg2, db_conf)
self.searcher = SphinxSearch(self.db)
self.expand_templ = template('aore/templates/postgre/expand_query.sql', aoid="//aoid")
self.normalize_templ = template('aore/templates/postgre/normalize_query.sql', aoid="//aoid")
# Проверка, что строка является действительым UUID v4
def __check_uuid(self, guid):
try:
UUID(guid)
except ValueError:
return False
return True
# Проверяет входящий параметр на соотвествие
# param - сам параметр
# rule - "boolean", "uuid", "text"
def __check_param(self, param, rule):
if rule == "boolean":
assert isinstance(param, bool), "Invalid parameter type"
if rule == "uuid":
assert (isinstance(param, str) or isinstance(param, unicode)) and self.__check_uuid(
param), "Invalid parameter value"
if rule == "text":
assert isinstance(param, str) or isinstance(param, unicode), "Invalid parameter type"
assert len(param) > 3, "Text too short"
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
assert pattern.match(param), "Invalid parameter value"
# text - строка поиска
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
def find(self, text, strong=False):
try:
text = urllib.unquote(text).decode('utf8')
self.__check_param(text, "text")
self.__check_param(strong, "boolean")
results = self.searcher.find(text, strong)
except Exception, err:
return dict(error=err.args[0])
return results
# Нормализует подаваемый AOID или AOGUID в актуальный AOID
def normalize(self, aoid_guid):
try:
self.__check_param(aoid_guid, "uuid")
sql_query = self.normalize_templ.replace("//aoid", aoid_guid)
rows = self.db.get_rows(sql_query, True)
except Exception, err:
return dict(error=err.args[0])
if len(rows) == 0:
return []
else:
return rows[0]
# Разворачивает AOID в представление (перед этим нормализует)
def expand(self, aoid_guid):
try:
self.__check_param(aoid_guid, "uuid")
normalized_id = self.normalize(aoid_guid)
assert 'aoid' in normalized_id, "AOID or AOGUID not found in DB"
normalized_id = normalized_id['aoid']
sql_query = self.expand_templ.replace("//aoid", normalized_id)
rows = self.db.get_rows(sql_query, True)
except Exception, err:
return dict(error=err.args[0])
return rows

160
aore/search/search.py Normal file
View File

@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
import re
import time
import Levenshtein
import sphinxapi
from aore.config import basic
from aore.config import sphinx_conf
from aore.search.wordentry import WordEntry
from aore.search.wordvariation import VariationType
from aore.miscutils.trigram import trigram
class SphinxSearch:
# Config's
delta_len = 2
default_rating_delta = 2
regression_coef = 0.08
max_result = 10
# Конфиги, которые в будущем, возможно, будут настраиваемы пользователем (как strong)
search_freq_words = True
def __init__(self, db):
self.db = db
sphinx_host = sphinx_conf.listen
sphinx_port = None
if ":" in sphinx_conf.listen and "unix:/" not in sphinx_conf.listen:
sphinx_host, sphinx_port = sphinx_conf.listen.split(":")
sphinx_port = int(sphinx_port)
self.client_sugg = sphinxapi.SphinxClient()
self.client_sugg.SetServer(sphinx_host, sphinx_port)
self.client_sugg.SetLimits(0, self.max_result)
self.client_sugg.SetConnectTimeout(3.0)
self.client_show = sphinxapi.SphinxClient()
self.client_show.SetServer(sphinx_host, sphinx_port)
self.client_show.SetLimits(0, self.max_result)
self.client_show.SetConnectTimeout(3.0)
def __configure(self, index_name, word_len):
self.client_sugg.ResetFilters()
if index_name == sphinx_conf.index_sugg:
self.client_sugg.SetRankingMode(sphinxapi.SPH_RANK_WORDCOUNT)
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.SetSelect("aoid, fullname, @weight-2*abs(wordcount-{}) AS krank".format(word_len))
self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
def __get_suggest(self, word, rating_limit, count):
word_len = str(len(word) / 2)
trigrammed_word = '"{}"/1'.format(trigram(word))
self.__configure(sphinx_conf.index_sugg, word_len)
result = self.client_sugg.Query(trigrammed_word, sphinx_conf.index_sugg)
# Если по данному слову не найдено подсказок (а такое бывает?)
# возвращаем []
if not result['matches']:
return []
maxrank = result['matches'][0]['attrs']['krank']
maxleven = None
outlist = list()
for match in result['matches']:
if len(outlist) >= count:
break
if maxrank - match['attrs']['krank'] < self.default_rating_delta:
jaro_rating = Levenshtein.jaro(word, match['attrs']['word'])
if not maxleven:
maxleven = jaro_rating - jaro_rating * self.regression_coef
if jaro_rating >= rating_limit and jaro_rating >= maxleven:
outlist.append([match['attrs']['word'], jaro_rating])
del jaro_rating
outlist.sort(key=lambda x: x[1], reverse=True)
return outlist
# Получает список объектов (слово)
def __get_word_entries(self, words, strong):
we_list = []
for word in words:
if word != '':
we_list.append(WordEntry(self.db, word))
return we_list
def find(self, text, strong):
def split_phrase(phrase):
phrase = unicode(phrase).lower()
return re.split(r"[ ,:.#$]+", phrase)
# сплитим текст на слова
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"
# получаем все вариации слов
all_variations = []
for we in word_entries:
for vari in we.variations_gen(strong, self.__get_suggest):
all_variations.append(vari)
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]))
self.__configure(sphinx_conf.index_addjobj, word_count)
# формируем строки для поиска в Сфинксе
for i in range(good_vars_word_count, max(0, good_vars_word_count - 3), -1):
first_q = "@fullname \"{}\"/{}".format(" ".join(good_var.text for good_var in good_vars), i)
if self.search_freq_words and freq_vars_word_count:
second_q = " @sname {}".format(" ".join(freq_var.text for freq_var in freq_vars))
self.client_show.AddQuery(first_q + second_q, sphinx_conf.index_addjobj)
del second_q
self.client_show.AddQuery(first_q, sphinx_conf.index_addjobj)
del first_q
start_t = time.time()
rs = self.client_show.RunQueries()
elapsed_t = time.time() - start_t
if basic.logging:
print(elapsed_t)
results = []
parsed_ids = []
for i in range(0, len(rs)):
for match in rs[i]['matches']:
if len(results) >= self.max_result:
break
if not match['attrs']['aoid'] in parsed_ids:
parsed_ids.append(match['attrs']['aoid'])
results.append(
dict(aoid=match['attrs']['aoid'],
text=unicode(match['attrs']['fullname']),
ratio=match['attrs']['krank'],
cort=i))
return results

153
aore/search/wordentry.py Normal file
View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
import re
from aore.config import sphinx_conf
from aore.search.wordvariation import WordVariation, VariationType
class WordEntry:
# Варианты распеределния для слов с первыми двумя символами, где:
# 0 - не найдено, 1 - найдено одно, x - найдено много (>1)
# 1st - кол-во слов по LIKE 'word%'
# 2nd - кол-во слов по точному совпадению
#
# 00 - не найдено ничего вообще. Опечатка или дряное слово. Ищем с подсказками (много)
# 01 - найдено одно точное совпадение, но нет лайков. Оставляем как есть.
# -0x - найдено много точных совпадений и... быть не может, там уник.
# 10 - найден один по лайку и ни одного точного. Недопечатка. * и немного подсказок.
# 11 - одно по лайку и одно точное. Нашли. Оставляем слово как есть.
# -1x - одно по лайку и много точных. Быть не может.
# x0 - много по лайку и нет точных. Недопечатка. Немного подсказок и *.
# x1 - много по лайку и один точный. Чет нашли. Как есть и *.
# xx - много по лайку и много точных. Оставляем как есть и *
#
# Теперь по сокращениям. Они работюат отдельно (ПОКА ЧТО)
# 3rd - кол-во слов по точному совпдению по полному сокращению.
# 4th - кол-во слов по точному совпадению по малому сокращению.
#
# 00 - ни найдено нигде. Значит, не сокращение (или с опечаткой). Не обрабатываем.
# 01 - найдено одно малое сокращение. Оставляем как есть (малые и так в словаре)
# 0x - найдено много малых. Не обрабатываем.
# 10 - найдено одно полное и 0 малых. Добавляем малое.
# 11 - найдено одно полное и одно малое. Бывает (допустим, 'сад'). Добавляем как есть.
# -1x - найдено одно полное и куча малых. Ну бред.
# x0 - найдено куча полных и ни одного малого. Добавляем малое.
# x1 - Куча полных и 1 малое. Хз, бывает ли. Не обрабатываем.
# xx - Куча полных и куча малых. Не обрабатываем.
match_types = dict(
MT_MANY_SUGG=['0000'],
MT_SOME_SUGG=['10..', 'x0..'],
MT_LAST_STAR=['100.', 'x.0.'],
MT_AS_IS=['.1..', '...1', '...x'],
MT_ADD_SOCR=['..10', '..x0'],
MT_IS_SOCR=['..01', '..0x']
)
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.bare_word = str(word)
self.word = self.__cleanify(self.bare_word)
self.word_len = len(unicode(self.word))
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
self.ranks = self.__init_ranks()
# Заполняем параметры слова
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)
# Если ищем по лайку, то точное совпадение не ищем (оно и так будет включено)
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:
self.MT_LAST_STAR = False
self.MT_AS_IS = True
def __cleanify(self, word):
return word.replace('-', '').replace('@', '')
def variations_gen(self, strong, suggestion_func):
default_var_type = VariationType.normal
# Если слово встречается часто, ставим у всех вариантов тип VariationType.freq
if self.parameters['IS_FREQ']:
default_var_type = VariationType.freq
# Добавляем подсказки (много штук)
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)
# Добавляем подсказки (немного)
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)
# Добавляем звездочку на конце
if self.MT_LAST_STAR:
yield WordVariation(self, self.word + '*', default_var_type)
# Добавляем слово "как есть", если это сокращение, то добавляем как частое слово
if self.MT_AS_IS:
var_t = default_var_type
if self.MT_IS_SOCR:
var_t = VariationType.freq
yield WordVariation(self, self.word, var_t)
# -- Дополнительные функции --
# Добавляем сокращение
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 '{}'" \
"UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(
self.word, self.word_len, self.word, self.bare_word, self.bare_word, self.word)
result = self.db.get_rows(sql_qry)
# Проставляем "сокращенное" сокращение, если нашли полное
if not self.parameters['SOCR_WORD']:
self.parameters['SOCR_WORD'] = result[2][1]
# Проверяем, если слово встречается слишком много раз
if len(result) == 5 and result[4][0] > 30000:
self.parameters['IS_FREQ'] = True
# Формируем список найденных величин совпадений:
# result[x]
# x = 0, поиск по неполному совпадению (лайк*), и по длине строки больше исходной
# x = 1, поиск по точному совпадению
# x = 2, поиск по базе сокращений (по полному)
# x = 3, то же, но по краткому
out_mask_list = []
for i in range(0, 4):
if result[i][0] > 1:
out_mask_list.append('x')
else:
out_mask_list.append(str(result[i][0]))
return ''.join(out_mask_list)
def get_type(self):
return ", ".join([x for x in self.match_types if self.__dict__[x]])
def __unicode__(self):
return self.word
def __str__(self):
return str(self.word)

View File

@@ -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