3333 *
3434 * @see https://supabase.com/docs/guides/ai/hybrid-search
3535 *
36- * @author Ahmed EBEN HASSINE <ahmedbhs123@æmail .com>
36+ * @author Ahmed EBEN HASSINE <ahmedbhs123@gmail .com>
3737 */
3838final readonly class PostgresHybridStore implements ManagedStoreInterface, StoreInterface
3939{
@@ -179,21 +179,17 @@ public function query(Vector $vector, array $options = []): array
179179 $ where = [];
180180 $ params = [];
181181
182- // Only add embedding param if we're doing vector search
183- if ($ semanticRatio > 0.0 ) {
184- $ params ['embedding ' ] = $ this ->toPgvector ($ vector );
185- }
186-
187182 // Use maxScore from options, or defaultMaxScore if configured
188183 $ maxScore = $ options ['maxScore ' ] ?? $ this ->defaultMaxScore ;
189184
185+ // Ensure embedding param is set if maxScore is used (regardless of semanticRatio)
186+ if ($ semanticRatio > 0.0 || null !== $ maxScore ) {
187+ $ params ['embedding ' ] = $ this ->toPgvector ($ vector );
188+ }
189+
190190 if (null !== $ maxScore ) {
191191 $ where [] = "( {$ this ->vectorFieldName } {$ this ->distance ->getComparisonSign ()} :embedding) <= :maxScore " ;
192192 $ params ['maxScore ' ] = $ maxScore ;
193- // Ensure embedding is available if maxScore is used
194- if (!isset ($ params ['embedding ' ])) {
195- $ params ['embedding ' ] = $ this ->toPgvector ($ vector );
196- }
197193 }
198194
199195 if (isset ($ options ['where ' ]) && '' !== $ options ['where ' ]) {
@@ -254,13 +250,7 @@ private function buildFtsOnlyQuery(string $whereClause, int $limit): string
254250 {
255251 // Add FTS match filter to ensure only relevant documents are returned
256252 $ ftsFilter = \sprintf ("content_tsv @@ websearch_to_tsquery('%s', :query) " , $ this ->language );
257-
258- if ('' !== $ whereClause ) {
259- // Combine existing WHERE clause with FTS filter
260- $ whereClause = str_replace ('WHERE ' , "WHERE $ ftsFilter AND " , $ whereClause );
261- } else {
262- $ whereClause = "WHERE $ ftsFilter " ;
263- }
253+ $ whereClause = $ this ->addFilterToWhereClause ($ whereClause , $ ftsFilter );
264254
265255 return \sprintf (<<<SQL
266256 SELECT id, %s AS embedding, metadata,
@@ -281,14 +271,8 @@ private function buildFtsOnlyQuery(string $whereClause, int $limit): string
281271 private function buildHybridQuery (string $ whereClause , int $ limit , float $ semanticRatio ): string
282272 {
283273 // Add FTS filter for the fts_scores CTE
284- $ ftsWhereClause = $ whereClause ;
285274 $ ftsFilter = \sprintf ("content_tsv @@ websearch_to_tsquery('%s', :query) " , $ this ->language );
286-
287- if ('' !== $ whereClause ) {
288- $ ftsWhereClause = str_replace ('WHERE ' , "WHERE $ ftsFilter AND " , $ whereClause );
289- } else {
290- $ ftsWhereClause = "WHERE $ ftsFilter " ;
291- }
275+ $ ftsWhereClause = $ this ->addFilterToWhereClause ($ whereClause , $ ftsFilter );
292276
293277 // RRF (Reciprocal Rank Fusion) - Same approach as Supabase
294278 // Formula: COALESCE(1.0 / (k + rank), 0.0) * weight
@@ -333,6 +317,30 @@ private function buildHybridQuery(string $whereClause, int $limit, float $semant
333317 );
334318 }
335319
320+ /**
321+ * Adds a filter condition to an existing WHERE clause using AND logic.
322+ *
323+ * @param string $whereClause Existing WHERE clause (may be empty or start with 'WHERE ')
324+ * @param string $filter Filter condition to add (without 'WHERE ')
325+ *
326+ * @return string Combined WHERE clause
327+ */
328+ private function addFilterToWhereClause (string $ whereClause , string $ filter ): string
329+ {
330+ if ('' === $ whereClause ) {
331+ return "WHERE $ filter " ;
332+ }
333+
334+ $ whereClause = rtrim ($ whereClause );
335+
336+ if (str_starts_with ($ whereClause , 'WHERE ' )) {
337+ return "$ whereClause AND $ filter " ;
338+ }
339+
340+ // Unexpected format, prepend WHERE
341+ return "WHERE $ filter AND " .ltrim ($ whereClause );
342+ }
343+
336344 private function toPgvector (VectorInterface $ vector ): string
337345 {
338346 return '[ ' .implode (', ' , $ vector ->getData ()).'] ' ;
0 commit comments