Skip to content

Instantly share code, notes, and snippets.

@denisxab
Created November 14, 2022 21:19
Show Gist options
  • Save denisxab/c523230d04ca58df754ad41d2e50ec39 to your computer and use it in GitHub Desktop.
Save denisxab/c523230d04ca58df754ad41d2e50ec39 to your computer and use it in GitHub Desktop.
fulltextserach.py Полнотекстовый поиск
import asyncio
import hashlib
from pprint import pprint
from typing import Any, Callable
import aiosqlite
import string
def sql_get_db(dbfile: str):
"""
Получить соединение с БД
dbfile: Путь к Бд SQLite
"""
def wrapper(func: Callable):
async def transaction_dec(*arg, **kwargs) -> tuple[bool | BaseException, Any]:
async with aiosqlite.connect(dbfile) as db:
kwargs['db'] = db
return await func(*arg, **kwargs)
return transaction_dec
return wrapper
async def sql_write(dbfile: str, query: str, args: dict = {}, RETURNING: bool = False) -> bool:
"""Выполнить команду на запись
:param dbfile: файл к БД
:param query: Команда
:param args: Аргументы в строку запроса
:param RETURNING: Нужно ли получать ответ после вставки записи
"""
async with aiosqlite.connect(dbfile) as db:
if RETURNING:
res = await db.execute(query, args)
r = await res.fetchone()
await db.commit()
return r
else:
await db.execute(query, args)
return await db.commit()
async def sql_read(dbfile: str, query: str, args: dict = {}) -> list[tuple]:
"""Выполнить команду на чтение
:param dbfile: файл к БД
:param query: SQL запрос
"""
async with aiosqlite.connect(dbfile) as db:
async with db.execute(query, args) as cursor:
return await cursor.fetchall()
class FullTextSearch():
@staticmethod
def preparation(text: str, char_filter: dict[str, str] = {}, tokenize: Callable = lambda: None, tokinfilter: Callable = lambda: None) -> tuple[str]:
text_after_char_filter = text.translate(char_filter)
text_after_tokenize = tokenize(text_after_char_filter)
text_after_tokinfilter = tuple(map(tokinfilter, text_after_tokenize))
return text_after_tokinfilter
def __init__(self, dbfile: str, char_filter: dict[str, str] = {}, tokenize: Callable = lambda: None, tokinfilter: Callable = lambda: None):
"""
dbfile: Путь к БД .sqlite
"""
self.dbfile = dbfile
self.char_filter = char_filter
self.tokenize = tokenize
self.tokinfilter = tokinfilter
# @profile
async def _add_word(self, text: str, data: str = ""):
"""
Добавить слово в таблицу
"""
text_after_tokinfilter: tuple[str] = self.preparation(
text, self.char_filter, self.tokenize, self.tokinfilter)
#
# Добавление или получения слов
#
# Ищем слова, если они уже существуют
q1_idword = await sql_read(self.dbfile, f"""
select word,idword from words
where word in ('{"','".join(text_after_tokinfilter)}');
""")
# Ищем слова которых нет в таблице
words_not_exist_from_table: set[str] = set(
x[0] for x in q1_idword).symmetric_difference(set(text_after_tokinfilter))
# Если есть слова, которые не существуют в таблице
if words_not_exist_from_table:
# То добавляем такие слова в БД
for w_n_e in words_not_exist_from_table:
# Добавление слов
_idword = await sql_write(
self.dbfile,
f"insert into words (word) VALUES (:word) RETURNING idword;",
{"word": w_n_e}, RETURNING=True
)
_idword = _idword[0] if _idword else []
q1_idword.append((w_n_e, _idword))
#
# Добавление текста, если он не существует
#
# Проверяем существование текста
hash256 = hashlib.sha256(
str(text_after_tokinfilter).encode('utf-8')).hexdigest()
q2_iddoc = await sql_read(self.dbfile, "select iddoc from docs where hash256=:hash256", {"hash256": hash256})
q2_iddoc = q2_iddoc[0][0] if len(q2_iddoc) > 0 else []
# Если такое документ не существует
if not q2_iddoc:
# 1. то добавляем данные связанные с текстом
iddata = await sql_write(self.dbfile, "insert into rawdata(data) values (:data) RETURNING iddata;", {"data": data}, RETURNING=True)
iddata = iddata[0] if len(iddata) > 0 else []
# 2. то добавляем такой документ
q2_iddoc = await sql_write(self.dbfile, "insert into docs(text,hash256,iddata) values (:text,:hash256,:iddata) RETURNING iddoc;", {"text": str(text_after_tokinfilter), "hash256": hash256, "iddata": iddata}, RETURNING=True)
q2_iddoc = q2_iddoc[0] if len(q2_iddoc) > 0 else []
#
# Создаем связку слова с текстом
# TODO: Надобы оптимезировать по времени, вставку слова
# "select * from link_to_doc where idword=:idword and iddoc=:iddoc"
for q in q1_idword:
await sql_write(
self.dbfile,
f"insert into link_to_doc(idword,iddoc) values (:idword,:iddoc);",
{"idword": q[1], "iddoc": q2_iddoc}
)
return True
async def add_words(self, args: list[tuple[str, str]]):
"""Добавить несколько слов
:param args: [(СловоДляПоиска,ПолезныеДанные)]
"""
for item in args:
await self._add_word(item[0], item[1])
async def search(self, text: str):
"""
Полнотекстовый поиск
"""
text_after_tokinfilter: list[str] = self.preparation(
text, self.char_filter, self.tokenize, self.tokinfilter)
query = f"""
-- Ранжирование результата
select
tmp.iddoc
,d.text
,tmp.count_search
,rd.data
from
(
select
l.iddoc -- Идентификатор документов
,count(w.idword) as count_search -- Сколько слов совпадает у этого документа
from words w
-- Получаем id документа в котором используется это слово
join link_to_doc l on l.idword=w.idword
where w.word in ('{"','".join(text_after_tokinfilter)}')
GROUP by l.iddoc
) tmp
join docs d on d.iddoc = tmp.iddoc
join rawdata rd on rd.iddata = d.iddata
order by count_search desc
"""
return await sql_read(self.dbfile, query)
async def _createBaseTable(self):
"""
Создать базовые таблицы
"""
words = """
create table if not exists words(
idword INTEGER PRIMARY KEY,
word text not null
);
"""
docs = """
create table if not exists docs(
iddoc INTEGER PRIMARY KEY,
hash256 text not null,
text text not null,
iddata INTEGER not null REFERENCES rawdata(iddata) on delete cascade
);
"""
link_to_doc = """
create table if not exists link_to_doc(
idword INTEGER not null REFERENCES words(idword) on delete cascade,
iddoc INTEGER not null REFERENCES docs(iddoc) on delete cascade,
primary key (idword,iddoc)
);
"""
rawdate = """
create table if not exists rawdata(
iddata INTEGER PRIMARY KEY,
data text not null
)
"""
indexs = [
"create unique index words_word ON words(word);", "create unique index docs_hash ON docs(hash256);"]
for table in words, docs, link_to_doc, *indexs, rawdate:
await sql_write(self.dbfile, table)
return True
def main():
analyze = dict(
char_filter={ord(x): "" for x in string.punctuation}, tokenize=lambda x: x.split(), tokinfilter=lambda x: x.lower()
)
o = FullTextSearch(
'/home/denis/DataGripProjects/vs/full_text_search.sqlite', **analyze
)
# asyncio.run(o._createBaseTable())
asyncio.run(Test.add1(o))
asyncio.run(Test.add2(o))
# return asyncio.run(Test.search(o))
class Test:
async def search(o):
return await o.search('носки из хлопка')
async def add1(o):
await o.add_words([
('''Строка текста — в изданиях на русском и европейских языках горизонтальный ряд слов, составная часть полосы набора. Различают С. т. начальную (абзаца, полосы, подраздела, произведения, издания), концевую (абзаца, полосы, подраздела, произведения, издания).''', 'Полезные данны 1',), ('''строка (строка текста) — 23.02.08 строка (строка текста) [line of text]: Последовательность знаков, как правило, состоящая из слов и пробелов, устанавливаемая при форматировании текста и построенная обычно по базовой (опорной) строке текста. Длина строки зависит от места ''', 'Полезные данны 2', ), (
'''красная строка — Отступ вправо в начале первой строки абзаца. Начинать писать как? с красной строки. «Сочинение начните писать с красной строки», – напомнил учитель. Красная строка – начальная строка текста или его части, начинающаяся с отступа. (Ю. Прохоров.)… ''', 'Полезные данны 3',)
])
async def add2(o):
await o.add_words([
('Набор мужских качественных носков. В упаковке 10 пар. Средние. Фото сделано вживую, что бы вы видели товар какой он есть, без всякого фотошопа.', 'https://www.ozon.ru/product/noski-696089670/?advert=HBSFkP5ZjMIM_8ITdMMaQn41Zx4opPMygl-fN6vBsTBknnzWTjVcK5kRmPEatS35Y2KFA_HhYjWnDFpvdfeX2m03Z-E6VG2P_LO6sMj-qru-woxCXG_XE3jeBCUXKG76PQ27fGFKNqaHDrtTxCz-gfRSzRYWFPsbETzsipWp3q1k6LY4JJem1cZ4L7phixpePXxS062CZabzsEhdo2awm2qtQBE_iCfK1hl9Yic8X6AEieO0ylpt7VrWHk6f-N8kMP9Y1gwNqH_2fFz4FhKoODSHkjZJvj72owqOQQHRLviPbL_xWKHM6WQD05AbGSyjipuabluaDwTWCriMakQkVo63YBW0GHrXj7PQgMrO5A675ztHIu6aXvQVbBpUiiCqcoetdw-bd4Av6bRDDPtpqMK6UgNsnIuEAhDzD3a_JvNAJLEtNtAqjD4qDkzwWI1FXAaRTeft4d8619U_lsNxCd6PQSnFZaixW0IPXMWDoOlLQ1w6372O8tZr7swb1XMSTMFHwz9ZYYpQOuSFjI03LTbsXymsDC0stzfIrZeHZzxgFhBUrVua7F6O8p2HQeh6KxUpxQO3lhR-1KC1V0vhksCbUbC5e9NLkWS-GisWx9OYWeyWMnb_C9JoOih2ZicFlNCDL46Q1AQl6tDJGL4TYivFqPhX3LpYiRPEwlL_TklysUe_7sztEgZSxayGyINP18PWLYQfjc0kQBBIYfJ7HCWJOgCkkqMcU6ske1_IFcU&avtc=1&avte=2&avts=1668458937&keywords=%D0%BD%D0%BE%D1%81%D0%BA%D0%B8&sh=VMxbMu0B7A'),
('Набор-комплект носков UP из хлопка с добавлением прочной эластичной нити 5 пар белых носков... Подходят для ежедневного использования и для занятий спортом. Идеальны для женщин и мужчин любого возраста. Особое плетение двубортной резинки в верхней части носка, не пережимает ногу и не препятствует кровообращению... Оптимальная высота изделия плюс плоский шов на мыске обеспечат удобство и комфорт вашим ногам. Носки из хлопка обладают повышенной воздухопроницаемостью. Не деформируются и не теряют цвет после стирки. Носки очень хорошо тянутся и подойдут даже тем, у кого размер чуть больше чем указан. Носите с удовольствием носки UP',
'https://www.ozon.ru/product/komplekt-noskov-up-5-par-543656918/?asb=RSTwvCf6DgrMZ3azwblSWAktTFq4Z9%252BdK5Dl%252FevUbXXLbXQCxUkImGn9fmcC3R%252FO&asb2=Zlwauu-vSX0TUkkRZ1hpA9fqtYbS77F_AAzV1p1wJSKASD6qq6btmPtYs0j1qwc4hu0c4gThrybEgf1cduJ9lz9jOxw_8-jR3V6Nty3PXuH6dRVnPDWgpspdujxabvaLOGicCfms97egVFyt1fG055vObOS7Zz0uynN4wV-cNEc&avtc=1&avte=2&avts=1668458937&keywords=%D0%BD%D0%BE%D1%81%D0%BA%D0%B8&sh=VMxbMh1jMA'),
('Магазины Найк закрылись? Не проблема! Длинные белые носки для вашего стильного образа. Набор высоких носков для женщин, мужчин, подростков для повседневной носки, спорта. В составе дышащий хлопок и эластан для комфорта ваших ног. Хорошо сохраняют форму и цвет, устойчивы в носке. Широкая резинка не перетягивает ногу, удобно ее облегает. Спортивная модель для тренировок, бега, фитнеса, йоги футбола. Модные носки с принтом подойдут на каждый день под любую обувь: кроссовки, кеды, ботинки. Хлопковые однотонные носки зимние, летние, демисезонные теплые можно носить девочке и мальчику в школу на физкультуру, на прогулки.',
'https://www.ozon.ru/product/noski-nike-noski-720796759/?asb=VTd3tLSN1t6rhrvovFLlGOkUPQ0YNl%252Ffn6xXBKk%252FWJZNv6qp1EEGbBViL3xV0zmB&asb2=P4IHr7KLJNFGa3UFHn6usRB7zBF4scLHPKS6bKf6KwPERHSBWTyAG8XVNkKyWLLx-XIxsrPY32u2u8PBNeS94Qrx2PysKyY2EnDeM7jnfwhDeLL180g_DsPSI-koPJXESFzeEeue6-NrSlsHyKfJ9A&avtc=1&avte=2&avts=1668458937&keywords=%D0%BD%D0%BE%D1%81%D0%BA%D0%B8&sh=VMxbMhV-Ug')
])
pprint(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment