+18
-9
backend/src/backend/api/search.py
+18
-9
backend/src/backend/api/search.py
···
4
4
5
5
from fastapi import APIRouter, Depends, Query
6
6
from pydantic import BaseModel
7
-
from sqlalchemy import func, select
7
+
from sqlalchemy import func, or_, select
8
8
from sqlalchemy.ext.asyncio import AsyncSession
9
9
10
10
from backend._internal.atproto.handles import search_handles
···
142
142
async def _search_tracks(
143
143
db: AsyncSession, query: str, limit: int
144
144
) -> list[TrackSearchResult]:
145
-
"""search tracks by title using trigram similarity."""
145
+
"""search tracks by title using trigram similarity + substring matching."""
146
146
# use pg_trgm similarity function for fuzzy matching
147
147
similarity = func.similarity(Track.title, query)
148
+
# also match substrings (e.g. "real" in "really")
149
+
substring_match = Track.title.ilike(f"%{query}%")
148
150
149
151
stmt = (
150
152
select(Track, Artist, similarity.label("relevance"))
151
153
.join(Artist, Track.artist_did == Artist.did)
152
-
.where(similarity > 0.1) # minimum similarity threshold
154
+
.where(or_(similarity > 0.1, substring_match))
153
155
.order_by(similarity.desc())
154
156
.limit(limit)
155
157
)
···
173
175
async def _search_artists(
174
176
db: AsyncSession, query: str, limit: int
175
177
) -> list[ArtistSearchResult]:
176
-
"""search artists by handle and display_name using trigram similarity."""
178
+
"""search artists by handle and display_name using trigram similarity + substring."""
177
179
# combine similarity scores from handle and display_name (take max)
178
180
handle_sim = func.similarity(Artist.handle, query)
179
181
name_sim = func.similarity(Artist.display_name, query)
180
182
combined_sim = func.greatest(handle_sim, name_sim)
183
+
# also match substrings
184
+
substring_match = or_(
185
+
Artist.handle.ilike(f"%{query}%"),
186
+
Artist.display_name.ilike(f"%{query}%"),
187
+
)
181
188
182
189
stmt = (
183
190
select(Artist, combined_sim.label("relevance"))
184
-
.where(combined_sim > 0.1)
191
+
.where(or_(combined_sim > 0.1, substring_match))
185
192
.order_by(combined_sim.desc())
186
193
.limit(limit)
187
194
)
···
204
211
async def _search_albums(
205
212
db: AsyncSession, query: str, limit: int
206
213
) -> list[AlbumSearchResult]:
207
-
"""search albums by title using trigram similarity."""
214
+
"""search albums by title using trigram similarity + substring."""
208
215
similarity = func.similarity(Album.title, query)
216
+
substring_match = Album.title.ilike(f"%{query}%")
209
217
210
218
stmt = (
211
219
select(Album, Artist, similarity.label("relevance"))
212
220
.join(Artist, Album.artist_did == Artist.did)
213
-
.where(similarity > 0.1)
221
+
.where(or_(similarity > 0.1, substring_match))
214
222
.order_by(similarity.desc())
215
223
.limit(limit)
216
224
)
···
235
243
async def _search_tags(
236
244
db: AsyncSession, query: str, limit: int
237
245
) -> list[TagSearchResult]:
238
-
"""search tags by name using trigram similarity."""
246
+
"""search tags by name using trigram similarity + substring."""
239
247
similarity = func.similarity(Tag.name, query)
248
+
substring_match = Tag.name.ilike(f"%{query}%")
240
249
241
250
# count tracks per tag
242
251
track_count_subq = (
···
252
261
similarity.label("relevance"),
253
262
)
254
263
.outerjoin(track_count_subq, Tag.id == track_count_subq.c.tag_id)
255
-
.where(similarity > 0.1)
264
+
.where(or_(similarity > 0.1, substring_match))
256
265
.order_by(similarity.desc())
257
266
.limit(limit)
258
267
)