@@ -130,25 +130,36 @@ def get_db_things(
130130
131131 # Querying logic
132132 #
133- # We combine multiple search strategies:
133+ # Combines multiple search strategies so the global search supports :
134134 #
135- # 1. Full-text search (tsvector)
136- # - Good for word-based and multi-word searches
137- # - Uses indexed search_vector column
135+ # 1. Full-text search using Thing.search_vector
136+ # - Best for normal word-based and multi-word searches.
138137 #
139- # 2. Trigram fuzzy matching (% operator from pg_trgm)
140- # - Handles typos (e.g. "Aron" vs "Aaron")
138+ # 2. Trigram fuzzy search using pg_trgm operators
139+ # - "%" compares similarity across the full string.
140+ # - "<%" compares similarity against a word/substring extent,
141+ # which helps with names like "cary" matching "john carry".
142+ #
143+ # 3. Partial substring search using ILIKE
144+ # - Handles very short partial inputs like "ty" matching "tyler".
145+ # - This is useful because trigram matching is less effective for
146+ # short queries under 3 characters.
141147 #
142148 # OR is used so any matching strategy can return a result.
143149 if query and query .strip ():
144150 clean_query = query .strip ()
151+ partial_query = f"%{ clean_query } %"
145152
146- # Similarity scores (used ONLY for ranking, not filtering)
153+ # Ranking scores
147154 #
148- # These use pg_trgm's similarity() to compute how close each field
149- # is to the search query. Higher = more similar.
150- name_sim = func .similarity (Thing .name , clean_query )
151- type_sim = func .similarity (Thing .thing_type , clean_query )
155+ # These are used only for ordering results, not for filtering.
156+ # similarity() ranks whole-string similarity.
157+ # word_similarity() ranks best word/span similarity.
158+ name_sim = func .coalesce (func .similarity (Thing .name , clean_query ), 0 )
159+ type_sim = func .coalesce (func .similarity (Thing .thing_type , clean_query ), 0 )
160+
161+ name_word_sim = func .coalesce (func .word_similarity (Thing .name , clean_query ), 0 )
162+ type_word_sim = func .coalesce (func .word_similarity (Thing .thing_type , clean_query ), 0 )
152163
153164 search_conditions = [
154165 Thing .search_vector .op ("@@" )(
@@ -159,27 +170,46 @@ def get_db_things(
159170 ),
160171 Thing .name .op ("%" )(clean_query ),
161172 Thing .thing_type .op ("%" )(clean_query ),
173+ Thing .name .op ("<%" )(clean_query ),
174+ Thing .thing_type .op ("<%" )(clean_query ),
175+ Thing .name .ilike (partial_query ),
176+ Thing .thing_type .ilike (partial_query ),
162177 ]
163178
164179 rank_expressions = [
165180 name_sim ,
166181 type_sim ,
182+ name_word_sim ,
183+ type_word_sim ,
167184 ]
168185
169186 if include_contacts :
170187 contact_sim = func .coalesce (func .similarity (Contact .name , clean_query ), 0 )
188+ contact_word_sim = func .coalesce (
189+ func .word_similarity (clean_query , Contact .name ), 0
190+ )
171191
172192 sql = sql .outerjoin (Thing .contact_associations ).outerjoin (
173193 ThingContactAssociation .contact
174194 )
175195
176- search_conditions .append (Contact .name .op ("%" )(clean_query ))
177- rank_expressions .append (contact_sim )
196+ search_conditions .extend (
197+ [
198+ Contact .name .op ("%" )(clean_query ),
199+ Contact .name .op ("<%" )(clean_query ),
200+ Contact .name .ilike (partial_query ),
201+ ]
202+ )
203+
204+ rank_expressions .extend (
205+ [
206+ contact_sim ,
207+ contact_word_sim ,
208+ ]
209+ )
178210
179- sql = (
180- sql .where (or_ (* search_conditions ))
181- .order_by (desc (func .greatest (* rank_expressions )))
182- .distinct (Thing .id )
211+ sql = sql .where (or_ (* search_conditions )).order_by (
212+ desc (func .greatest (* rank_expressions ))
183213 )
184214
185215 if include_contacts :
@@ -238,7 +268,7 @@ def get_db_things(
238268
239269 sql = order_sort_filter (sql , Thing , sort , order , filters = merged_filters )
240270
241- return paginate (query = sql , conn = session )
271+ return paginate (query = sql , conn = session , unique = True )
242272
243273
244274def get_thing_type_from_request (request : Request ) -> str :
0 commit comments