etadevosyan commited on
Commit
00887f1
1 Parent(s): 67f1cc2

First commit

Browse files
Files changed (5) hide show
  1. app.py +43 -0
  2. backup/search_data_23-08-2024.json +0 -0
  3. requirements.txt +7 -0
  4. search.json +0 -0
  5. 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