Переделан слегка индекс, наброски алго поиска с сокращениями
This commit is contained in:
parent
9f67e139d6
commit
955c72c01c
26
README.md
26
README.md
@ -135,18 +135,22 @@ _Внимание_! Только Python 2.7, только PostgreSQL, тольк
|
|||||||
## Настройка
|
## Настройка
|
||||||
### Первоначальная настройка базы данных
|
### Первоначальная настройка базы данных
|
||||||
1. Настроим конфиг, он лежит в `aore/config/__init__.py`, в этом файле можно изменить `.dev` на `.prod`,
|
1. Настроим конфиг, он лежит в `aore/config/__init__.py`, в этом файле можно изменить `.dev` на `.prod`,
|
||||||
отредактировать, соотвественно, dev.py или prod.py: прописать параметры доступа к базе и путь,
|
отредактировать, соотвественно, dev.py или prod.py: прописать параметры доступа к базе и путь,
|
||||||
куда будут сохраняться данные Sphinx; по этому пути дополнительно необходимо создать 3 папки: log, run и data
|
куда будут сохраняться данные Sphinx; по этому пути дополнительно необходимо создать 3 папки: log, run и data
|
||||||
|
|
||||||
2. Создадим базу:
|
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.rar`
|
||||||
`sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked` - из директории.
|
- из директории `sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked`
|
||||||
`sudo -u phias python manage.py -b create -s http` - онлайн, с сервера ФНС.
|
- онлайн, с сервера ФНС `sudo -u phias python manage.py -b create -s http`
|
||||||
Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version <num>`, где num -
|
Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version <num>`, где num -
|
||||||
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`
|
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`.
|
||||||
|
|
||||||
3. Проиндексируем Sphinx:
|
3. Проиндексируем Sphinx:
|
||||||
Windows: `python manage.py -c -i C://sphinx//indexer.exe -o C://sphinx//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`
|
- Debian: `sudo python manage.py -c -i indexer -o /usr/local/sphinx/etc/sphinx.conf`
|
||||||
|
|
||||||
4. Затем запустим searchd:
|
4. Затем запустим searchd:
|
||||||
Windows: `net start sphinxsearch`, при этом файл настройки должен быть доступен Sphinx'у.
|
- Windows: `net start sphinxsearch`, при этом файл настройки должен быть доступен Sphinx'у.
|
||||||
Debian: `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf`
|
- Debian: `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf`
|
||||||
|
|
||||||
5. Настроим WSGI server, я использую nginx + passenger, Вы можете использовать любое приемлемое сочетание.
|
5. Настроим WSGI server, я использую nginx + passenger, Вы можете использовать любое приемлемое сочетание.
|
@ -1,31 +1,27 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
import Levenshtein
|
import Levenshtein
|
||||||
import sphinxapi
|
import sphinxapi
|
||||||
import time
|
|
||||||
|
|
||||||
|
from aore.config import basic
|
||||||
from aore.config import sphinx_conf
|
from aore.config import sphinx_conf
|
||||||
from aore.fias.wordentry import WordEntry
|
from aore.fias.wordentry import WordEntry
|
||||||
|
from aore.fias.wordvariation import VariationType
|
||||||
from aore.miscutils.trigram import trigram
|
from aore.miscutils.trigram import trigram
|
||||||
from aore.config import basic
|
|
||||||
|
|
||||||
|
|
||||||
class SphinxSearch:
|
class SphinxSearch:
|
||||||
# Config's
|
# Config's
|
||||||
delta_len = 2
|
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
|
default_rating_delta = 2
|
||||||
regression_coef = 0.08
|
regression_coef = 0.08
|
||||||
max_result = 10
|
max_result = 10
|
||||||
|
|
||||||
exclude_freq_words = True
|
# Конфиги, которые в будущем, возможно, будут настраиваемы пользователем (как strong)
|
||||||
|
search_freq_words = True
|
||||||
|
|
||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -39,16 +35,17 @@ class SphinxSearch:
|
|||||||
self.client_show.SetLimits(0, self.max_result)
|
self.client_show.SetLimits(0, self.max_result)
|
||||||
self.client_show.SetConnectTimeout(3.0)
|
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()
|
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.SetRankingMode(sphinxapi.SPH_RANK_WORDCOUNT)
|
||||||
self.client_sugg.SetFilterRange("len", int(wlen) - self.delta_len, int(wlen) + self.delta_len)
|
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, wlen))
|
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")
|
self.client_sugg.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
|
||||||
else:
|
else:
|
||||||
self.client_show.SetRankingMode(sphinxapi.SPH_RANK_BM25)
|
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):
|
def __get_suggest(self, word, rating_limit, count):
|
||||||
word_len = str(len(word) / 2)
|
word_len = str(len(word) / 2)
|
||||||
@ -83,35 +80,12 @@ class SphinxSearch:
|
|||||||
|
|
||||||
return outlist
|
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):
|
def __get_word_entries(self, words, strong):
|
||||||
we_list = []
|
we_list = []
|
||||||
for word in words:
|
for word in words:
|
||||||
if word != '':
|
if word != '':
|
||||||
we = WordEntry(self.db, word)
|
we_list.append(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)
|
|
||||||
|
|
||||||
return we_list
|
return we_list
|
||||||
|
|
||||||
@ -123,19 +97,44 @@ class SphinxSearch:
|
|||||||
# сплитим текст на слова
|
# сплитим текст на слова
|
||||||
words = split_phrase(text)
|
words = split_phrase(text)
|
||||||
|
|
||||||
# получаем список объектов
|
# получаем список объектов (слов)
|
||||||
word_entries = self.__get_word_entries(words, strong)
|
word_entries = self.__get_word_entries(words, strong)
|
||||||
word_count = len(word_entries)
|
word_count = len(word_entries)
|
||||||
|
|
||||||
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
|
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
|
||||||
assert word_count > 0, "No legal words is specified"
|
assert word_count > 0, "No legal words is specified"
|
||||||
|
|
||||||
# формируем строки для поиска в Сфинксе
|
# получаем все вариации слов
|
||||||
for x in range(word_count, max(0, word_count - 3), -1):
|
all_variations = []
|
||||||
self.client_show.AddQuery("\"{}\"/{} \"ул \"/0".format(" ".join(x.get_variations() for x in word_entries), x),
|
for we in word_entries:
|
||||||
sphinx_conf.index_addjobj)
|
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()
|
start_t = time.time()
|
||||||
rs = self.client_show.RunQueries()
|
rs = self.client_show.RunQueries()
|
||||||
@ -154,8 +153,9 @@ class SphinxSearch:
|
|||||||
if not ma['attrs']['aoid'] in parsed_ids:
|
if not ma['attrs']['aoid'] in parsed_ids:
|
||||||
parsed_ids.append(ma['attrs']['aoid'])
|
parsed_ids.append(ma['attrs']['aoid'])
|
||||||
results.append(
|
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
|
return results
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aore.config import basic
|
|
||||||
from aore.config import sphinx_conf
|
from aore.config import sphinx_conf
|
||||||
|
from aore.fias.wordvariation import WordVariation, VariationType
|
||||||
|
|
||||||
|
|
||||||
class WordEntry:
|
class WordEntry:
|
||||||
@ -42,42 +42,68 @@ class WordEntry:
|
|||||||
MT_ADD_SOCR=['..10', '..x0']
|
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):
|
def __init__(self, db, word):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.word = str(word)
|
self.word = str(word)
|
||||||
self.word_len = len(unicode(self.word))
|
self.word_len = len(unicode(self.word))
|
||||||
self.variations = []
|
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
|
||||||
self.scname = None
|
self.ranks = self.__init_ranks()
|
||||||
self.is_freq_word = False
|
|
||||||
self.ranks = self.__get_ranks()
|
|
||||||
|
|
||||||
for x, y in self.match_types.iteritems():
|
# Заполняем параметры слова
|
||||||
self.__dict__[x] = False
|
for mt_name, mt_values in self.match_types.iteritems():
|
||||||
for z in y:
|
self.__dict__[mt_name] = False
|
||||||
self.__dict__[x] = self.__dict__[x] or re.search(z, self.ranks) is not None
|
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:
|
if self.MT_LAST_STAR:
|
||||||
self.MT_AS_IS = False
|
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_LAST_STAR = False
|
||||||
self.MT_AS_IS = True
|
self.MT_AS_IS = True
|
||||||
|
|
||||||
def add_variation_socr(self):
|
def variations_gen(self, strong, suggestion_func):
|
||||||
if self.scname:
|
default_var_type = VariationType.normal
|
||||||
self.add_variation(self.scname)
|
# Если слово встречается часто, ставим у всех вариантов тип 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:
|
if self.MT_SOME_SUGG and not strong:
|
||||||
return self.variations[0]
|
suggs = suggestion_func(self.word, self.rating_limit_hard, self.rating_limit_hard_count)
|
||||||
return "{}".format(" ".join(self.variations))
|
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) > {} " \
|
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(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \
|
||||||
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
|
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
|
||||||
@ -88,12 +114,12 @@ class WordEntry:
|
|||||||
result = self.db.get_rows(sql_qry)
|
result = self.db.get_rows(sql_qry)
|
||||||
|
|
||||||
# Проставляем "сокращенное" сокращение, если нашли полное
|
# Проставляем "сокращенное" сокращение, если нашли полное
|
||||||
if not self.scname:
|
if not self.parameters['SOCR_WORD']:
|
||||||
self.scname = result[2][1]
|
self.parameters['SOCR_WORD'] = result[2][1]
|
||||||
|
|
||||||
# Проверяем, если слово встречается слишком много раз
|
# Проверяем, если слово встречается слишком много раз
|
||||||
if len(result) == 5 and result[4][0] > 30000:
|
if len(result) == 5 and result[4][0] > 30000:
|
||||||
self.is_freq_word = True
|
self.parameters['IS_FREQ'] = True
|
||||||
|
|
||||||
# Формируем список найденных величин совпадений:
|
# Формируем список найденных величин совпадений:
|
||||||
# result[x]
|
# result[x]
|
||||||
@ -118,4 +144,3 @@ class WordEntry:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.word)
|
return str(self.word)
|
||||||
|
|
||||||
|
14
aore/fias/wordvariation.py
Normal file
14
aore/fias/wordvariation.py
Normal 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
|
@ -9,6 +9,6 @@
|
|||||||
PATH.fullname || ', ' || child.shortname || ' ' || child.formalname AS fullname
|
PATH.fullname || ', ' || child.shortname || ' ' || child.formalname AS fullname
|
||||||
FROM "ADDROBJ" AS child
|
FROM "ADDROBJ" AS child
|
||||||
, PATH
|
, 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)
|
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)
|
@ -10,9 +10,9 @@ source {{index_name}}
|
|||||||
sql_query = {{!sql_query}}
|
sql_query = {{!sql_query}}
|
||||||
|
|
||||||
sql_field_string = fullname
|
sql_field_string = fullname
|
||||||
|
sql_attr_uint = len
|
||||||
sql_attr_string = aoid
|
sql_attr_string = aoid
|
||||||
sql_attr_string = aoguid
|
sql_attr_string = aoguid
|
||||||
sql_attr_uint = aolevel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index {{ index_name }}
|
index {{ index_name }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user