From 1a572cc1720076d9ea3f3417b421cfaf4aec863b Mon Sep 17 00:00:00 2001 From: white <122345776@umail.ucc.ie> Date: Tue, 11 Feb 2025 16:38:46 +0000 Subject: [PATCH] FEAT: Implemented working search bar using FTS and Ranking --- frontend/src/components/Layout/SearchBar.tsx | 23 +++++++--- web_server/blueprints/__init__.py | 4 +- web_server/blueprints/search_bar.py | 45 +++++++++++-------- web_server/database/app.db | Bin 159744 -> 159744 bytes web_server/utils/utils.py | 7 ++- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/Layout/SearchBar.tsx b/frontend/src/components/Layout/SearchBar.tsx index f21a46f..93f3da1 100644 --- a/frontend/src/components/Layout/SearchBar.tsx +++ b/frontend/src/components/Layout/SearchBar.tsx @@ -18,17 +18,28 @@ const SearchBar: React.FC = () => { // Perform search when debounced query changes useEffect(() => { if (debouncedQuery.trim()) { - fetch(`/api/search/${debouncedQuery}`) - .then((response) => response.json()) - .then((data) => { + const fetchSearchResults = async () => { + try { + const response = await fetch("/api/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: debouncedQuery }), // <-- Fixed payload + }); + + const data = await response.json(); console.log("Search results:", data); // Handle the search results here - }) - .catch((error) => { + } catch (error) { console.error("Error performing search:", error); - }); + } + }; + + fetchSearchResults(); // Call the async function } }, [debouncedQuery]); + const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value); diff --git a/web_server/blueprints/__init__.py b/web_server/blueprints/__init__.py index a6361fe..be59ce4 100644 --- a/web_server/blueprints/__init__.py +++ b/web_server/blueprints/__init__.py @@ -12,7 +12,8 @@ from blueprints.chat import chat_bp from blueprints.oauth import oauth_bp, init_oauth from blueprints.socket import socketio from celery import Celery -from celery_tasks import celery_init_app +from celery_tasks import celery_init_app# +from blueprints.search_bar import search_bp from os import getenv @@ -69,6 +70,7 @@ def create_app(): app.register_blueprint(stream_bp) app.register_blueprint(chat_bp) app.register_blueprint(oauth_bp) + app.register_blueprint(search_bp) socketio.init_app(app) diff --git a/web_server/blueprints/search_bar.py b/web_server/blueprints/search_bar.py index 48a5588..2f70cf4 100644 --- a/web_server/blueprints/search_bar.py +++ b/web_server/blueprints/search_bar.py @@ -1,15 +1,19 @@ -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, request from database.database import Database +from utils.utils import sanitize search_bp = Blueprint("search", __name__) -@search_bp.route("/search/", methods=["GET", "POST"]) -def search_results(query: str): +@search_bp.route("/search", methods=["POST"]) +def search_results(): """ Return the most similar search results This is the main route that displays a subsection of each search topic """ + data = request.get_json() + query = sanitize(data["query"]) + # Create the connection to the database db = Database() db.create_connection() @@ -17,32 +21,37 @@ def search_results(query: str): # Get the most accurate search results # 3 categories categories = db.fetchall(""" - SELECT bm25(category_fts), rank, f.category_id, f.category_name - FROM categories AS c - INNER JOIN category_fts AS f ON c.category_id = f.category_id - WHERE category_fts MATCH ? - LIMIT 3; + SELECT bm25(category_fts) AS score, c.category_id, c.category_name + FROM categories AS c + INNER JOIN category_fts AS f ON c.category_id = f.category_id + WHERE f.category_name LIKE '%' || ? || '%' + ORDER BY score ASC + LIMIT 3; """, (query,)) # 3 users users = db.fetchall(""" - SELECT bm25(user_fts), rank, f.user_id, f.username, f.is_live - FROM users u - INNER JOIN user_fts f ON u.user_id = f.user_id - WHERE user_fts MATCH ? - LIMIT 3; + SELECT bm25(user_fts) AS score, u.user_id, u.username, u.is_live + FROM users AS u + INNER JOIN user_fts AS f ON u.user_id = f.user_id + WHERE f.username LIKE '%' || ? || '%' + ORDER BY score ASC + LIMIT 3; """, (query,)) # 3 streams streams = db.fetchall(""" - SELECT bm25(stream_fts), rank, f.user_id, f.title, f.num_viewers, f.category_id - FROM streams s - INNER JOIN stream_fts f ON s.user_id = f.user_id - WHERE stream_fts MATCH ? - LIMIT 3; + SELECT bm25(stream_fts) AS score, s.user_id, s.title, s.num_viewers, s.category_id + FROM streams AS s + INNER JOIN stream_fts AS f ON s.user_id = f.user_id + WHERE f.title LIKE '%' || ? || '%' + ORDER BY score ASC + LIMIT 3; """, (query,)) db.close_connection() + + print(query, streams, users, categories, flush=True) return jsonify({"categories": categories, "users": users, "streams": streams}) diff --git a/web_server/database/app.db b/web_server/database/app.db index dee097635e4fc6412060706de859cd27fc359469..7ff7e7329db425e794f8822de5cb80b7451915c4 100644 GIT binary patch delta 956 zcmb_aOH5Ni6rDp~d2OlhJpnCJC{z#%tjO!Xt$--PE|ti-DUR=VY;xm(q_OiYR9k2o438nS8_a$Z%h8Hw>nJPkn0K zWz3h_1&c1n{K_1cGjUl`*w^u~?P@|nCREg80BTMKGK?u~%Y?_gkrW%+)Sw^7LG3mi z5SlG(izmv&W%S;Ly-k-X(kEN%-oiGKGmh4(L2*-(>9pi7=IY%iq-RyrJjNcRG;t8KNog(duB_lp**6~isz&h2|c zec|3vCn<4s=ypc^=&@?HGj9}#%t*Z(b#gJTS`rpPt4swv0d0b0m+9G}ok z&KQ@nh#F5|8TG|bf+o5UgBvTfQZ~Jfp){pYq|I)WqwqIuYaCuQ(0CjU;#}F9O`7!e zqQ&W?sTeY8-i>r_M^4a+I$GnwLDbSi4=x}`?G;!+fVO(^3^i2hd9qon9do(OmIVuQnbAENB}8FuH3~8{ScyG5#8tn5k&n6g6Q_6Ki!~+f*|TgL^t|2qs#a4^71~<^SG2T9DFH` zmfK?-$9Y&dS(rG^Jdp9uLhRzkcSqbR zx@@hy)2xnP9JMLjx6EkFNGq5Cfk<6^VyM#d^Z3 z34Y5@OH2NCOA8Zm+$0w2Ke_yJrQ)#g3AbKF>ql^gPuA#~VLE^S;Gy6_e8zODPN9ZX z4Tn%hT`6YMG&-GvpD_%YNN-b^VR8$!&W}Dm0ex!jO>mDlJRW8yO%a^c)ijo3o`3M; zUiRf(bTfbs<5V5KfILC$rWc1%iAq8m3)nsurs0K)cBav2?$;;Mg&^FRoMoN#K8X3| hEqWRC)nhs;vS2A)tH%tMUezOd^#QZb;79TO{x8Uu^hf{z diff --git a/web_server/utils/utils.py b/web_server/utils/utils.py index b767ec7..3c2f833 100644 --- a/web_server/utils/utils.py +++ b/web_server/utils/utils.py @@ -37,7 +37,7 @@ def get_most_popular_category() -> Optional[List[dict]]: return category -def sanitize(user_input: str, input_type="username") -> str: +def sanitize(user_input: str, input_type="default") -> str: """ Sanitizes user input based on the specified input type. @@ -63,6 +63,11 @@ def sanitize(user_input: str, input_type="username") -> str: "min_length": 8, "max_length": 256, }, + "default": { + "pattern": r"^[\S]+$", # Non-whitespace characters only + "min_length": 1, + "max_length": 50, + }, } # Get the validation rules for the specified type