Spaces:
Sleeping
Sleeping
etadevosyan
commited on
Commit
•
00887f1
1
Parent(s):
67f1cc2
First commit
Browse files- app.py +43 -0
- backup/search_data_23-08-2024.json +0 -0
- requirements.txt +7 -0
- search.json +0 -0
- search.py +133 -0
app.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import gradio as gr
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from search import search_bm25, search_exact, prepare_data, merge_results
|
5 |
+
import os
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
data = prepare_data()
|
10 |
+
|
11 |
+
HF_TOKEN = os.getenv('HF_TOKEN')
|
12 |
+
hf_writer = gr.HuggingFaceDatasetSaver(HF_TOKEN, "budu_search_data_new")
|
13 |
+
|
14 |
+
def search_handler(query: str):
|
15 |
+
results, exact_results = (
|
16 |
+
search_bm25(query, data),
|
17 |
+
search_exact(query, data)
|
18 |
+
)
|
19 |
+
|
20 |
+
json_results = merge_results(exact_results, results)
|
21 |
+
return {'results': json_results}
|
22 |
+
|
23 |
+
def create_ui(query):
|
24 |
+
recommendations = []
|
25 |
+
results = search_handler(query)
|
26 |
+
for result in results['results'][:5]:
|
27 |
+
recommendations.append(f"<div style='padding: 10px; border-bottom: 1px solid #ddd;'>{result['name']}</div>")
|
28 |
+
|
29 |
+
return gr.HTML(f"<div style='max-height: 400px; overflow-y: auto;'>{''.join(recommendations)}</div>")
|
30 |
+
|
31 |
+
iface = gr.Interface(
|
32 |
+
fn=create_ui,
|
33 |
+
inputs=gr.Textbox(label="Введите запрос"),
|
34 |
+
outputs=gr.HTML(), # Use HTML to render custom styled output,
|
35 |
+
allow_flagging='manual',
|
36 |
+
flagging_callback = gr.CSVLogger(),
|
37 |
+
flagging_options = ['Хорошая рекомендаация',
|
38 |
+
'Плохая рекомендаация'],
|
39 |
+
title="Поисковая система BUDU",
|
40 |
+
)
|
41 |
+
|
42 |
+
iface.launch()
|
43 |
+
|
backup/search_data_23-08-2024.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn
|
3 |
+
python-dotenv
|
4 |
+
rank_bm25
|
5 |
+
transformers
|
6 |
+
pandas
|
7 |
+
psycopg2-binary
|
search.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
search.py
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import AutoTokenizer
|
2 |
+
from rank_bm25 import BM25Okapi
|
3 |
+
import json
|
4 |
+
import re
|
5 |
+
|
6 |
+
tokenizer = AutoTokenizer.from_pretrained("deepvk/USER-bge-m3")
|
7 |
+
|
8 |
+
|
9 |
+
with open('search.json', 'r', encoding='utf-8') as file:
|
10 |
+
data = json.load(file)
|
11 |
+
|
12 |
+
|
13 |
+
def prepare_data(data=data):
|
14 |
+
for item in data:
|
15 |
+
|
16 |
+
corpus = [item['name'].lower()] + [x.lower() for x in item['synonyms']]
|
17 |
+
corpus = " ".join(corpus)
|
18 |
+
|
19 |
+
item['corpus'] = corpus
|
20 |
+
item['tokenized_corpus'] = tokenizer.tokenize(corpus)
|
21 |
+
|
22 |
+
return data
|
23 |
+
|
24 |
+
|
25 |
+
def clean_text(text):
|
26 |
+
"""
|
27 |
+
Очищает текст от специальных символов, чтобы можно было искать по содержимому.
|
28 |
+
"""
|
29 |
+
return re.sub(r'[^\w\s]', '', text).lower()
|
30 |
+
|
31 |
+
def search_bm25(query, data):
|
32 |
+
"""
|
33 |
+
Выполняет поиск по запросу с использованием BM25 + токенизатора.
|
34 |
+
|
35 |
+
query: строка, поисковый запрос.
|
36 |
+
data: список словарей, содержащих информацию о каждом элементе (name, id, synonyms, tokenized_corpus).
|
37 |
+
|
38 |
+
return: список словарей с уникальными name, id и score.
|
39 |
+
"""
|
40 |
+
|
41 |
+
tokenized_corpus = [item['tokenized_corpus'] for item in data]
|
42 |
+
bm25 = BM25Okapi(tokenized_corpus)
|
43 |
+
|
44 |
+
tokenized_query = tokenizer.tokenize(query)
|
45 |
+
doc_scores = bm25.get_scores(tokenized_query)
|
46 |
+
|
47 |
+
|
48 |
+
results = []
|
49 |
+
for idx in reversed(doc_scores.argsort()):
|
50 |
+
item = data[idx]
|
51 |
+
if item['name'] not in [result['name'] for result in results] and doc_scores[idx] > 0:
|
52 |
+
results.append({
|
53 |
+
'name': item['name'],
|
54 |
+
'id': item['id'],
|
55 |
+
'score': float(doc_scores[idx])
|
56 |
+
})
|
57 |
+
|
58 |
+
return results
|
59 |
+
|
60 |
+
def search_exact(query, data):
|
61 |
+
"""
|
62 |
+
Выполняет точный поиск подстроки в 'name' и 'synonyms'.
|
63 |
+
Слово либо является началом слова, либо целым словом.
|
64 |
+
|
65 |
+
"""
|
66 |
+
results = []
|
67 |
+
cleaned_query = clean_text(query)
|
68 |
+
|
69 |
+
|
70 |
+
query_regex = re.compile(r'\b' + re.escape(cleaned_query) + r'\b|\b' + re.escape(cleaned_query)) # Выбираем только целые слова или начала слов (чтобы не искать подстроки)
|
71 |
+
# Сначала ищем подстроку в 'name'
|
72 |
+
for item in data:
|
73 |
+
cleaned_name = clean_text(item['name'])
|
74 |
+
if query_regex.search(cleaned_name):
|
75 |
+
results.append({
|
76 |
+
'name': item['name'],
|
77 |
+
'id': item['id'],
|
78 |
+
'score': 1 # В данном случае score не важен, но можно оставить
|
79 |
+
})
|
80 |
+
|
81 |
+
# Затем ищем подстроку в 'synonyms'
|
82 |
+
for item in data:
|
83 |
+
for synonym in item['synonyms']:
|
84 |
+
cleaned_synonym = clean_text(synonym)
|
85 |
+
if query_regex.search(cleaned_synonym):
|
86 |
+
if not any(res['id'] == item['id'] for res in results): # Избегаем дублирования
|
87 |
+
results.append({
|
88 |
+
'name': item['name'],
|
89 |
+
'id': item['id'],
|
90 |
+
'score': 1
|
91 |
+
})
|
92 |
+
break # Достаточно найти одно совпадение среди синонимов
|
93 |
+
|
94 |
+
return results
|
95 |
+
|
96 |
+
#TODO: Добавить поиск подкатегорий
|
97 |
+
|
98 |
+
|
99 |
+
def merge_results(results1, results2):
|
100 |
+
"""
|
101 |
+
Объединяет два списка результатов с чередованием элементов, избегая дубликатов.
|
102 |
+
|
103 |
+
results1: первый список результатов (список словарей с ключами 'name', 'id', 'score')
|
104 |
+
results2: второй список результатов (список словарей с ключами 'name', 'id', 'score')
|
105 |
+
|
106 |
+
return: объединённый список результатов
|
107 |
+
"""
|
108 |
+
merged_results = []
|
109 |
+
seen_ids = set() # Набор для отслеживания уникальных id
|
110 |
+
|
111 |
+
# Чередуем элементы из двух списков
|
112 |
+
for res1, res2 in zip(results1, results2):
|
113 |
+
# Добавляем элемент из первого списка, если его id ещё не было
|
114 |
+
if res1['id'] not in seen_ids:
|
115 |
+
merged_results.append(res1)
|
116 |
+
seen_ids.add(res1['id'])
|
117 |
+
|
118 |
+
# Добавляем элемент из второго списка, если его id ещё не было
|
119 |
+
if res2['id'] not in seen_ids:
|
120 |
+
merged_results.append(res2)
|
121 |
+
seen_ids.add(res2['id'])
|
122 |
+
|
123 |
+
for res in results1[len(results2):]:
|
124 |
+
if res['id'] not in seen_ids:
|
125 |
+
merged_results.append(res)
|
126 |
+
seen_ids.add(res['id'])
|
127 |
+
|
128 |
+
for res in results2[len(results1):]:
|
129 |
+
if res['id'] not in seen_ids:
|
130 |
+
merged_results.append(res)
|
131 |
+
seen_ids.add(res['id'])
|
132 |
+
|
133 |
+
return merged_results
|