[SQL] Sorteren op relevantie zonder MATCH en AGAINST

Pagina: 1
Acties:
  • 147 views sinds 30-01-2008
  • Reageer

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
Ik ben bezig met het maken van een php-class waarmee je in een mysql tabel kan zoeken.
Als ik bijvoorbeeld "klaas piet" als zoekwoorden opgeef, dan wil ik dat hij (natuurlijk) klaas en piet laat zien. Maar als klaas en piet in één record voor zou komen, dan moet deze record als eerste door de query uitgepoept worden. Kortom; het resultaat moet gesorteerd worden op relevantie.

Als sql query krijg ik nu ongeveer dit:
SQL:
1
2
3
4
5
SELECT * FROM users
WHERE
(name LIKE '%klaas%' OR address LIKE '%klaas%' OR email LIKE '%klaas%' OR city LIKE '%klaas%' )
OR
(name LIKE '%piet%' OR address LIKE '%piet%' OR email LIKE '%piet%' OR city LIKE '%piet%' )


Maar heeft iemand een idee hoe ik ervoor kan zorgen dat records die beide zoekwoorden hebben, naar boven gesorteerd worden?

NB: Full-text search is geen optie, simpelweg omdat het iets te beperkt is.

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 22-01 23:51

NMe

Quia Ego Sic Dico.

Je vindt fulltext search te beperkt, en gaat daarom maar aan de slag met like, wat nóg beperkter is? Mis ik iets? ;)

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.


  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Als je relevantie wil toepassen moet je toch écht een Full Text Search gebruiken ;)
Er zijn wel andere constructies te bedenken, maar die zijn vergeleken met een FTS niet echt rendabel. Zo zou je een UDF kunnen maken die een "relevantie" teruggeeft en daarop kunnen orderen.

[ Voor 19% gewijzigd door RobIII op 03-10-2006 17:07 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


  • Yoozer
  • Registratie: Februari 2001
  • Laatst online: 20-01 22:02

Yoozer

minimoog

DemonTPx schreef op dinsdag 03 oktober 2006 @ 16:55:
Kortom; het resultaat moet gesorteerd worden op relevantie.
Kortom, je moet een gewicht aan je termen weten te hangen. Keyword search is een leuk onderwerp; des te leuker omdat er op GoT vrijwel altijd wordt verwezen naar een F/OSS oplossing zoals Lucene of ht://Dig.
Maar heeft iemand een idee hoe ik ervoor kan zorgen dat records die beide zoekwoorden hebben, naar boven gesorteerd worden?
Splits je documenten in velden van verschillende waardes. Splits die velden weer in aparte woordjes. Voer frequentie-analyse uit. Voer lengte-normalisatie uit (een lang document met 5 termen heeft een kleinere relevantie dan een kort document met 5 termen). Voer idf uit (inverse document frequency; log ( aantal documenten / documenten waarin de term voorkomt). Mik dit vervolgens in een index-tabel, en ga -daar- op zoeken. Extragratis tip: ga dit vooral NIET proberen in 1 query/interface/script te stoppen, want dat vermoord je server :P

Bonuspunten als je een stemming-algoritme toepast zodat termen zoals "programmeren", "programmeur" en "programma" allemaal gecondenseerd worden naar 1 enkele term :).

teveel zooi, te weinig tijd


  • gvanh
  • Registratie: April 2003
  • Laatst online: 02-12-2023

gvanh

Webdeveloper

Genoemde oplossingen (vooral de laatste) zijn enorm prachtige oplossingen, maar impliceren dat je een heel eigen zoek/indexerings engine gaat bouwen ... dat kost nogal wat tijd, inspanning en vooral veel programmeer-kennis.

Zelf heb ik een snellere oplossing gekozen, die tot nog toe prima werkt. Wel kost die oplossing meer systeem resources en moet je verzekeren dat je een beperkt aantal results bekijkt, hetzij door een maximum aantal results te tonen, of door je results per pagina te beperken.

Wat ik doe is eerst op de door jou genoemde manier een zoekopdracht geven in MySQL. Daarna doorloop ik in PHP de resultaten en voer ik de relevantie berekeningen daar uit. Als ik me goed herinner gebruik ik daar preg_match voor. Als bonus heb ik dan de mogelijkheid om - analoog aan de Google resultaten - ook meteen de gevonden zoektermen in context te laten zien bij de zoek resultaten. Je krijgt dan zoiets als.

Gevonden titel
... de eerste paar woorden voor de zoekterm en een paar woorden er na ...

Uiteraard is dit - zie genoemde argumenten - niet de ideale methode en kost moet je goed blijven opletten dat je je server niet gaat overbelasten ... maar voor mij werkt het prima. Zoekresultaten op een database met 50.000+ records worden nog steeds ruim onder 500 ms. op het scherm getoond.

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
Het project wat ik aan het maken ben is niet groot genoeg om een compleet eigen search engine te maken. Of om allemaal zoek-informatie in een index tabel te gooien of wat dan ook.

Fulltext search is niet handig, omdat het minimaal 3 characters nodig heeft en alleen maar op volledige woorden matcht.

De laatste oplossing lijkt me voor nu in ieder geval de beste oplossing. Ik heb namelijk toch niet het idee dat ik een mega database krijg.

Bedankt voor de hulp mensen! :)

Verwijderd

...kuch.... wildcard (*) ...kuch...

  • Boss
  • Registratie: September 1999
  • Laatst online: 11:31

Boss

+1 Overgewaardeerd

Maak een functie die 1 geeft als A en B voorkomen en 0 als dat niet zo is. Daar op sorteren en klaar :)

The process of preparing programs for a digital computer is especially attractive, not only because it can be economically and scientifically rewarding, but also because it is an aesthetic experience much like composing poetry or music.


  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
Boss schreef op woensdag 04 oktober 2006 @ 10:27:
Maak een functie die 1 geeft als A en B voorkomen en 0 als dat niet zo is. Daar op sorteren en klaar :)
En hoe zou ik dat moeten maken? In de sql query?
Verwijderd schreef op woensdag 04 oktober 2006 @ 10:04:
...kuch.... wildcard (*) ...kuch...
Ja dat je die kan gebruiken bij fulltext search, weet ik.. maar dat weten de eindgebruikers niet... dus dat is ook geen oplossing.

Verwijderd

DemonTPx schreef op woensdag 04 oktober 2006 @ 11:42:
Ja dat je die kan gebruiken bij fulltext search, weet ik.. maar dat weten de eindgebruikers niet... dus dat is ook geen oplossing.
Dan plak je die toch zelf achter je zoekstring ;)

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
Verwijderd schreef op woensdag 04 oktober 2006 @ 11:53:
[...]
Dan plak je die toch zelf achter je zoekstring ;)
haha zoiets?
PHP:
1
preg_replace("/\b([^\s]+)\b/", "*$1*", $searchstring);


zou kunnen.. maarja.. ik heb al een complete class gemaakt die dus query's genereerd zoals in mn startpost..
en ten tweede zit die restrictie er nog op, dat je minimaal 3 characters moet opgeven... (of telt hij de sterretjes ook mee als characters?)

  • GX
  • Registratie: Augustus 2000
  • Laatst online: 14-05-2025

GX

Nee.

DemonTPx schreef op woensdag 04 oktober 2006 @ 12:12:
[...]

haha zoiets?
PHP:
1
preg_replace("/\b([^\s]+)\b/", "*$1*", $searchstring);


zou kunnen.. maarja.. ik heb al een complete class gemaakt die dus query's genereerd zoals in mn startpost..
en ten tweede zit die restrictie er nog op, dat je minimaal 3 characters moet opgeven... (of telt hij de sterretjes ook mee als characters?)
Sorry?! Dit lijkt nergens op. Hoe kom je er bij om zoiets met preg_replace te doen?

Edit: Wacht, hint: $str .= "*"; of $str = '*'.$str.'*';, of sprintf($str, "*%s*", $str); of desnoods in je query. En er zijn nog meer methodes.

[ Voor 11% gewijzigd door GX op 04-10-2006 12:38 ]


  • jvdmeer
  • Registratie: April 2000
  • Laatst online: 00:37
Ik doe dit even uit mijn hoofd (Er komen wel vaker kromme oplossingen uit mijn hoofd)

SQL:
1
2
3
4
5
6
7
8
SELECT ID, COUNT(*) FROM
(
SELECT ID FROM users WHERE (name LIKE '%klaas%' OR address LIKE '%klaas%' OR email LIKE '%klaas%' OR city LIKE '%klaas%' )
UNION ALL
SELECT ID FROM users WHERE (name LIKE '%piet%' OR address LIKE '%piet%' OR email LIKE '%piet%' OR city LIKE '%piet%' )
) x
GROUP BY ID
ORDER BY COUNT(*) DESC

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
GX schreef op woensdag 04 oktober 2006 @ 12:36:
[...]


Sorry?! Dit lijkt nergens op. Hoe kom je er bij om zoiets met preg_replace te doen?

Edit: Wacht, hint: $str .= "*"; of $str = '*'.$str.'*';, of sprintf($str, "*%s*", $str); of desnoods in je query. En er zijn nog meer methodes.
Omdat het om meerdere keywords in een string gaat.. voortaan moet je eerst even op onderzoek uit gaan, voordat je gaat zeggen dat het nergens op lijkt... http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html


jvdmeer schreef op woensdag 04 oktober 2006 @ 14:53:

Ik doe dit even uit mijn hoofd (Er komen wel vaker kromme oplossingen uit mijn hoofd)

SQL:
1
2
3
4
5
6
7
8
SELECT ID, COUNT(*) FROM
(
SELECT ID FROM users WHERE (name LIKE '%klaas%' OR address LIKE '%klaas%' OR email LIKE '%klaas%' OR city LIKE '%klaas%' )
UNION ALL
SELECT ID FROM users WHERE (name LIKE '%piet%' OR address LIKE '%piet%' OR email LIKE '%piet%' OR city LIKE '%piet%' )
) x
GROUP BY ID
ORDER BY COUNT(*) DESC
wat doet het? :?

edit:
Ok ik snap wat het doet hehe.. deze is handig.. harstikke bedankt :D

[ Voor 4% gewijzigd door DemonTPx op 04-10-2006 15:17 ]


  • jvdmeer
  • Registratie: April 2000
  • Laatst online: 00:37
Ik weet niet of MySql dit slikt, maar MS-SQL begrijpt onderstaande. Voorwaarde voor een juiste uitvoering is natuurlijk de aanwezigheid van het unieke veld ID.

SQL:
1
2
3
4
5
6
7
8
9
10
11
SELECT aantal, * FROM users
JOIN
(
SELECT ID, COUNT(*) as aantal FROM
(
SELECT ID FROM users WHERE (name LIKE '%klaas%' OR address LIKE '%klaas%' OR email LIKE '%klaas%' OR city LIKE '%klaas%' )
UNION ALL
SELECT ID FROM users WHERE (name LIKE '%piet%' OR address LIKE '%piet%' OR email LIKE '%piet%' OR city LIKE '%piet%' )
) x
GROUP BY ID) y on users.ID=y.ID
ORDER BY y.aantal DESC

  • Genoil
  • Registratie: Maart 2000
  • Laatst online: 12-11-2023
Ik ben hier ook eens een tijdje mee bezig geweest en postte destijds ook al hierover. Ik ben altijd van plan geweest er een mooie tutorial van te gaan maken maar dat is er nooit van gekomen. Wel heb ik een demootje online. Die errored alleen bij geen resultaten en hij cached heel smerig alle output, dus zoeken op 'god' is niet veel meer dan een readfile($cache) ;) . Maar voor de rest werkt het alleraardigst.

Hier ook nog de relevante sourcecode, de base-classes moet je er zelf maar bij verzinnen ;)
Waar het op neerkomt is dat ik het MYSQL fulltext algoritme heb nagebouwd in PHP, en er alleen net iets meer mee kan. Betekent wel dat je per tabel met zoekbare text een extra veld heb die de fulltext index nadoet (waar dan met LIKE in word gezocht), en nog 2 magische getallen waarvan ik ook niet meer precies weet hoe ze ok al weer werkten, $norm_pivot en $sum_dtf.

[edit]
De demo staat trouwens standaard op AND, en ik zie ook dat als je die via de URL op OR zet, de weging geen extra punten toekend aan AND. Maar dat moest ik idd nog doen :) Maar goed. Daar gaat het niet om :P. Of ja daar gaat het eigenlijk wel om maar ik heb geen zin om dat nu gaan veranderen ;)


PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<?
import("nl.shapers.search.LikeSearch");

function order_objects_by_relevance_desc($a, $b) {
   if ($a->relevance == $b->relevance) {
       return 0;
   }
   return ($a->relevance > $b->relevance) ? -1 : 1;
}

class FtLikeSearch extends LikeSearch {
    
    var $variantFactors = array("exact" => 2,
                                "start" => 1.5,
                                "end"   => 1, 
                                "mid"   => 0.5);
    
    var $variants       = array();
    
    function FtLikeSearch() {
        LikeSearch::LikeSearch();
    }
        
    function setStopwords($file) {
        
        global $stopwords;
        
        if(!isset($GLOBALS['stopwords'])) {
            $ext = end(explode(".",$file));
            if($ext == "php") {
                include($file);
            }
            elseif($ext == "txt") {
                $phpfile    = preg_replace("/txt$/", "php", $file);
                if(file_exists($phpfile)) {
                    include($phpfile);
                }
                else {
                    $contents   = file_get_contents($file);
                    $regexes    = "/(\s|^)(".str_replace(",","(\s|$))+/,/(\s|^)(", $contents).")(\s|$)/";
                    $stopwords  = explode("," , $regexes);
                    FtLikeSearch::compileStopwords($phpfile);
                }
            }
        }
        return is_array($GLOBALS['stopwords']);
    }
    
    function compileStopwords($file) {
        $contents = "<?\n\$stopwords = array(\"".implode("\",\"", $GLOBALS["stopwords"])."\");\n?>";
        $fp = fopen($file, "w");
        fwrite($fp, $contents);
        fclose($fp);
    }
    
    function setQuery($query, $stopwordsFile = SEARCH_STOPWORDS_FILE) {

        BaseSearch::setQuery($query);
        $this->setStopwords($stopwordsFile);
        $stoppedQuery = preg_replace($GLOBALS['stopwords'], "", $this->query);
        if(strlen($stoppedQuery) > 0) {
            $this->words = explode(" ", $stoppedQuery);     
            
            foreach($this->words as $n => $word) {
                $this->words[$n] = strtolower($word);
                $this->variants[$n] = array();
                $this->variants[$n]["exact"] = " ".$word." ";
                $this->variants[$n]["start"] = " ".$word;
                $this->variants[$n]["end"]   = $word." ";
                $this->variants[$n]["mid"]   = $word;
            }
            $this->setPregWords($this->words);
        }
        else {
            $this->words = null;
        }
    }
                        
    function getResults(& $count, $from = null, $to = null) {
        if(SEARCH_DB_ENGINE == "Propel") {
            $results = BaseSearch::getResultsPropel();
        }
        else {
            $results = BaseSearch::getResults();
        }
        
        if($this->merge) {
            //$results = $this->mergeResults($results);
        }
        
        foreach($results as $class => $objects) {
            $count[$class] = count($objects);
            if(count($objects) > 0) {
                $G = 0;
                $objects = $this->setupWeights($objects, $this->words, $this->properties[$class]["table"], $G);

                //$GLOBALS['timer']->sample("objects weighed");
                foreach($objects as $n => $object) {
                    $objects[$n]->relevance = $this->calculateRelevance($object, $G);
                }
                //$GLOBALS['timer']->sample("objects ranked");
                usort($objects, "order_objects_by_relevance_desc");
                if($from !== null && $to !== null) {
                    $results[$class] = array_slice ($objects, $from, $to-$from + 1);
                }
                else {
                    $results[$class] = $objects;
                }
            }
        }
        //$GLOBALS['timer']->sample("objects ordered");
        return $results;
    }
    
    function getObjectFtVars($object, $ft_fields) {
        $sw = !isset($GLOBALS['stopwords']) ? FtLikeSearch::setStopwords(SEARCH_STOPWORDS_FILE) 
                                            : is_array($GLOBALS['stopwords']);
                
        $fulltext = "";
        foreach ($ft_fields as $field) {
            eval("\$data = \$object->get".ucfirst(camelCase($field))."();");
            $words = str_replace(array("-", "\r\n", "\n", "\r"), " ", strtolower(strip_tags($data)));       
            $words = preg_replace("/[^a-z0-9\s\t]/", "", $words);
            
            if($sw) {
                
                $words = preg_replace($GLOBALS['stopwords'], " ", $words);
            }
            $fulltext.= $words." ";
        }
        
        $unique_words = FtLikeSearch::getUniqueWords($fulltext);

        $sum_dtf      = FtLikeSearch::getSumDtf($fulltext, $unique_words);
        $U            = sizeof($unique_words);
        $norm_pivot   = $U/(1 + 0.0115 * $U);

        return array("fulltext" => $fulltext, "sum_dtf" => $sum_dtf, "norm_pivot" => $norm_pivot );
    }
        
    function getUniqueWords($fulltext) {
        return array_keys(array_flip(str_word_count($fulltext, 1)));
    }
    
    function getSumDtf($fulltext, $unique_words) {
        
        $sum_dtf = 0;
        for($n = 0; $n < count($unique_words); $n++) {
            $dtf  = substr_count($fulltext, $unique_words[$n]);
            $sum_dtf += log((float)$dtf) + 1;
        }
        
        return $sum_dtf;
    } 
        
    function setupWeights($objects, $words, $table, & $G) {
        $lcquery    = strtolower($this->query);
                    
        for($n = 0; $n < count($objects); $n++) {
            
            $objects[$n]->LNqf = array();
        
            $ft         = $objects[$n]->ft;
            $sum_dtf    = $objects[$n]->sum_dtf;
            $norm_pivot = $objects[$n]->norm_pivot;
            
            foreach($words as $i => $word) {
                foreach($this->variants[$i] as $type => $variant) {
                    $dtf    = substr_count($ft, $variant);
                    if($dtf > 0) break;
                }                   
                $vf     = $this->variantFactors[$type];
                
                $qf     = substr_count($lcquery, $word);
                $L      = (log((float)$dtf) + 1) / $sum_dtf;
                
                $N      = $norm_pivot;
                
                $objects[$n]->LNqf[] = $L * $N * $qf * $vf;
            }
        }           
        $row = mysql_query("SELECT count(*) AS rows FROM ".$table." LIMIT 1") or die (mysql_error());
        $rowCountResult = mysql_fetch_object($row);
        
        $N  =  $rowCountResult->rows;
        $nf =  count($objects);
        
        
        $G  = log((float)(($N - $nf) / $nf)); // IDFP;  
        //  $G  = log((float)($N / $nf));   // IDF;
        
        return $objects;        
    }
        
    function calculateRelevance($object, $G) {  
        $relevance  = 0;
                
        foreach($object->LNqf as $LNqf) {
            $R = $LNqf * $G;
            if(sizeof($object->LNqf) == 1 && $R < 0) $R = -$R;
            if(is_finite($R))
                $relevance += $R;
        }
            
        return $relevance;
    }
}

?>

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
Na een klein beetje onderzoek weer een ontdekking: zoeken met fulltext search (zonder boolean mode) werkt niet met sterretjes of procent-tekentjes. Hiervoor is dus echt boolean mode nodig... en in boolean mode heb je dus niet echt een mooi ranking systeem..

In boolean mode heeft een sterretje aan het begin van een woord heeft geen enkele invloed :S

MySQL begint een beetje vervelend te worden imo.. Dus de aanpak die ik ongeveer zal moeten gaan gebruiken is een soort combinatie van de dingen die Yoozer en gvanh gezegt hebben.

  • moozzuzz
  • Registratie: Januari 2005
  • Niet online
of een andere database-engine...

  • DemonTPx
  • Registratie: December 2002
  • Laatst online: 05-12-2025
..zoals... geef eens wat voorbeelden met query's enzo

  • Genoil
  • Registratie: Maart 2000
  • Laatst online: 12-11-2023
DemonTPx schreef op donderdag 05 oktober 2006 @ 15:33:
Na een klein beetje onderzoek weer een ontdekking: zoeken met fulltext search (zonder boolean mode) werkt niet met sterretjes of procent-tekentjes. Hiervoor is dus echt boolean mode nodig... en in boolean mode heb je dus niet echt een mooi ranking systeem..

In boolean mode heeft een sterretje aan het begin van een woord heeft geen enkele invloed :S
Ja dit zijn dus precies de redenen waarom ik m'n eigen fulltext implementatie hebt geschreven. Het relevantiealgoritme komt gewoon rechtstreeks uit de MySQL source maar dan met toevoegingen...
Pagina: 1