1- <?php declare (strict_types = 1 );
1+ <?php declare (strict_types= 1 );
22
33namespace Quest \Macros ;
44
5+ use Illuminate \Database \Query \Builder ;
56use Quest \Matchers \ExactMatcher ;
67use Illuminate \Support \Facades \DB ;
78use Quest \Matchers \AcronymMatcher ;
89use Quest \Matchers \InStringMatcher ;
910use Quest \Matchers \StudlyCaseMatcher ;
10- use Illuminate \Database \Query \Builder ;
1111use Quest \Matchers \StartOfWordsMatcher ;
1212use Quest \Matchers \StartOfStringMatcher ;
1313use Quest \Matchers \TimesInStringMatcher ;
@@ -22,52 +22,140 @@ class WhereFuzzy
2222 *
2323 **/
2424 protected static array $ matchers = [
25- ExactMatcher::class => 100 ,
26- StartOfStringMatcher::class => 50 ,
27- AcronymMatcher::class => 42 ,
25+ ExactMatcher::class => 100 ,
26+ StartOfStringMatcher::class => 50 ,
27+ AcronymMatcher::class => 42 ,
2828 ConsecutiveCharactersMatcher::class => 40 ,
29- StartOfWordsMatcher::class => 35 ,
30- StudlyCaseMatcher::class => 32 ,
31- InStringMatcher::class => 30 ,
32- TimesInStringMatcher::class => 8 ,
29+ StartOfWordsMatcher::class => 35 ,
30+ StudlyCaseMatcher::class => 32 ,
31+ InStringMatcher::class => 30 ,
32+ TimesInStringMatcher::class => 8 ,
3333 ];
3434
35+ /**
36+ * Construct a fuzzy search expression.
37+ *
38+ **/
39+ public static function make (Builder $ builder , $ field , $ value ): Builder
40+ {
41+ $ value = static ::escapeValue ($ value );
42+ $ nativeField = '` ' . str_replace ('. ' , '`.` ' , trim ($ field , '` ' )) . '` ' ;
3543
44+ if (!is_array ($ builder ->columns ) || empty ($ builder ->columns )) {
45+ $ builder ->columns = ['* ' ];
46+ }
47+ $ builder
48+ ->addSelect ([static ::pipeline ($ field , $ nativeField , $ value )])
49+ ->having ('fuzzy_relevance_ ' . str_replace ('. ' , '_ ' , $ field ), '> ' , 0 );
50+
51+ static ::calculateTotalRelevanceColumn ($ builder );
52+ return $ builder ;
53+ }
3654
3755 /**
3856 * Construct a fuzzy search expression.
3957 *
4058 **/
41- public static function make (Builder $ builder , $ field , $ value ) : Builder
59+ public static function makeOr (Builder $ builder , $ field , $ value ): Builder
4260 {
43- $ value = str_replace (['" ' , "' " , '` ' ], '' , $ value );
44-
45- $ native = '` ' . str_replace ('. ' , '`.` ' , trim ($ field , '` ' )) . '` ' ;
46- $ value = substr (DB ::connection ()->getPdo ()->quote ($ value ), 1 , -1 );
4761
48- if (! is_array ($ builder ->columns ) || empty ($ builder ->columns )) {
62+ $ value = static ::escapeValue ($ value );
63+ $ nativeField = '` ' . str_replace ('. ' , '`.` ' , trim ($ field , '` ' )) . '` ' ;
64+
65+ if (!is_array ($ builder ->columns ) || empty ($ builder ->columns )) {
4966 $ builder ->columns = ['* ' ];
5067 }
68+ $ builder
69+ ->addSelect ([static ::pipeline ($ field , $ nativeField , $ value )])
70+ ->orHaving ('fuzzy_relevance_ ' . str_replace ('. ' , '_ ' , $ field ), '> ' , 0 );
71+
72+ static ::calculateTotalRelevanceColumn ($ builder );
5173
52- return $ builder
53- ->addSelect (static ::pipeline ($ field , $ native , $ value ))
54- ->orderBy ('relevance_ ' . str_replace ('. ' , '_ ' , $ field ), 'desc ' )
55- ->having ('relevance_ ' . str_replace ('. ' , '_ ' , $ field ), '> ' , 0 );
74+ return $ builder ;
5675 }
5776
77+ /**
78+ * Manage relevance columns SUM for total relevance ORDER
79+ * Searches all relevance columns and parses the relevance expressions to create the total relevance column
80+ * and creates the order statement for it
81+ * @param $builder
82+ * @return bool
83+ */
84+ protected static function calculateTotalRelevanceColumn ($ builder ): bool
85+ {
86+ if (!empty ($ builder ->columns )) {
87+ $ existingRelevanceColumns = [];
88+ $ sumColumnIdx = null ;
89+ // search for fuzzy_relevance_* columns and _fuzzy_relevance_ position
90+ foreach ($ builder ->columns as $ as => $ column ) {
91+ if ($ column instanceof Expression) {
92+ if (stripos ($ column ->getValue (), 'AS fuzzy_relevance_ ' )) {
93+ $ matches = [];
94+ preg_match ('/AS (fuzzy_relevance_.*)$/ ' , $ column ->getValue (), $ matches );
95+ if (!empty ($ matches [1 ])) {
96+ $ existingRelevanceColumns [$ as ] = $ matches [1 ];
97+ }
98+ } elseif (stripos ($ column ->getValue (), 'AS _fuzzy_relevance_ ' )) {
99+ $ sumColumnIdx = $ as ;
100+ }
101+ }
102+ }
103+ // glue together all relevance expresions under _fuzzy_relevance_ column
104+ $ relevanceTotalColumn = '' ;
105+ foreach ($ existingRelevanceColumns as $ as => $ column ) {
106+ $ relevanceTotalColumn .= (!empty ($ relevanceTotalColumn ) ? ' + ' : '' )
107+ . '( '
108+ . str_ireplace (' AS ' . $ column , '' , $ builder ->columns [$ as ]->getValue ())
109+ . ') ' ;
110+ }
111+ $ relevanceTotalColumn .= ' AS _fuzzy_relevance_ ' ;
112+
113+ if (is_null ($ sumColumnIdx )) {
114+ // no sum column yet, just add this one
115+ $ builder
116+ ->addSelect ([new Expression ($ relevanceTotalColumn )]);
117+ } else {
118+ // update the existing one
119+ $ builder ->columns [$ sumColumnIdx ] = new Expression ($ relevanceTotalColumn );
120+ }
121+ // only add the _fuzzy_relevance_ ORDER once
122+ if (
123+ !$ builder ->orders
124+ || ($ builder ->orders
125+ && array_search ('_fuzzy_relevance_ ' ,
126+ array_column ($ builder ->orders , 'column ' )
127+ ) === false
128+ )
129+ ) {
130+ $ builder ->orderBy ('_fuzzy_relevance_ ' , 'desc ' );
131+ }
132+ return true ;
133+ }
134+ return false ;
135+ }
58136
137+ /**
138+ * Escape value input for fuzzy search
139+ * @param $value
140+ * @return false|string
141+ */
142+ protected static function escapeValue ($ value )
143+ {
144+ $ value = str_replace (['" ' , "' " , '` ' ], '' , $ value );
145+ $ value = substr (DB ::connection ()->getPdo ()->quote ($ value ), 1 , -1 );
146+ return $ value ;
147+ }
59148
60149 /**
61150 * Execute each of the pattern matching classes to generate the required SQL.
62151 *
63152 **/
64- protected static function pipeline ($ field , $ native , $ value ) : Expression
153+ protected static function pipeline ($ field , $ native , $ value ): Expression
65154 {
66155 $ sql = collect (static ::$ matchers )->map (
67- fn ($ multiplier , $ matcher ) =>
68- (new $ matcher ($ multiplier ))->buildQueryString ("COALESCE( $ native, '') " , $ value )
156+ fn ($ multiplier , $ matcher ) => (new $ matcher ($ multiplier ))->buildQueryString ("COALESCE( $ native, '') " , $ value )
69157 );
70158
71- return DB ::raw ($ sql ->implode (' + ' ) . ' AS relevance_ ' . str_replace ('. ' , '_ ' , $ field ));
159+ return DB ::raw ($ sql ->implode (' + ' ) . ' AS fuzzy_relevance_ ' . str_replace ('. ' , '_ ' , $ field ));
72160 }
73161}
0 commit comments