├── manual_commands.txt ├── make_mod ├── sphinx_plugin ├── scripts │ └── create_links ├── config │ └── sphinx_stopwords.txt ├── language │ └── en │ │ └── mods │ │ └── fulltext_sphinx.php └── includes │ ├── functions_sphinx.php │ ├── sphinxapi-0.9.8.php │ └── search │ └── fulltext_sphinx.php └── sphinx_mod ├── install.xml ├── license.txt └── modx.prosilver.en.xsl /manual_commands.txt: -------------------------------------------------------------------------------- 1 | ### Initial Index 2 | # Replace {SALT} with your avatar salt 3 | # 4 | 5 | # Index main 6 | indexer --config /confpath/sphinx.conf index_phpbb_{SALT}_main >> /logpath/indexer.log 2>&1 & 7 | # Index delta 8 | indexer --config /confpath/sphinx.conf index_phpbb_{SALT}_delta >> /logpath/indexer.log 2>&1 & 9 | 10 | ### Start searchd 11 | searchd --config /etc/sphinx/sphinx.conf >> /var/log/sphinx/searchd-startup.log 2>&1 & 12 | 13 | ### Seldom reindex 14 | # Initial index commands with --rotate before --config 15 | 16 | ### Frequent reindex 17 | indexer --rotate --config /etc/sphinx/sphinx.conf index_phpbb_{SALT}_delta >> /var/log/sphinx/indexer.log 2>&1 & -------------------------------------------------------------------------------- /make_mod: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | print_help() { 4 | echo "Usage: make_mod MOD_VERSION" 5 | exit 1 6 | } 7 | 8 | mod_version="$1" 9 | mod_name="sphinx4phpbb-$mod_version" 10 | 11 | if [ "$1" = "--help" -o "$1" = "-h" -o "$1" == "" ]; then 12 | print_help 13 | fi 14 | 15 | if [ -e "$mod_name" ]; then 16 | echo "$mod_name exists, delete before running this script" 17 | print_help 18 | fi 19 | 20 | if [ -e "$mod_name.tar.bz2" ]; then 21 | echo "$mod_name.tar.bz2 exists, delete before running this script" 22 | print_help 23 | fi 24 | 25 | mkdir -p $mod_name/root 26 | 27 | cp sphinx_mod/* $mod_name/ 28 | cp -r sphinx_plugin/includes $mod_name/root/ 29 | cp -r sphinx_plugin/language $mod_name/root/ 30 | cp -r sphinx_plugin/config $mod_name/ 31 | 32 | find $mod_name/ -type d -name ".svn" -exec echo -n 'deleted ' \; -print -exec rm -rf {} \; 33 | find $mod_name/ -type f -name "*~" -exec echo -n 'deleted ' \; -print -exec rm {} \; 34 | 35 | tar -cjvf $mod_name.tar.bz2 $mod_name 36 | rm -rf $mod_name -------------------------------------------------------------------------------- /sphinx_plugin/scripts/create_links: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | checkdirs=("includes" "includes/search" "language/en/mods") 4 | files=("includes/sphinxapi-0.9.8.php" "includes/functions_sphinx.php" "includes/search/fulltext_sphinx.php" "language/en/mods/fulltext_sphinx.php") 5 | 6 | print_help() { 7 | echo "Usage: create_links PATH_TO_PHPBB [PATH_TO_SPHINX_PLUGIN]" 8 | exit 1 9 | } 10 | 11 | plugin_path="$2" 12 | phpbb_path="$1" 13 | 14 | if [ "$1" = "--help" -o "$1" = "-h" -o "$1" == "" ]; then 15 | print_help 16 | fi 17 | 18 | if [ "$plugin_path" == "" ]; then 19 | plugin_path="../" 20 | fi 21 | 22 | if [ "$phpbb_path" == "" ]; then 23 | phpbb_path="." 24 | fi 25 | 26 | if [ ! -e "$phpbb_path" ]; then 27 | echo "Could not find $phpbb_path" 28 | print_help 29 | fi 30 | 31 | if [ ! -e "$plugin_path" ]; then 32 | echo "Could not find $plugin_path" 33 | print_help 34 | fi 35 | 36 | absolute_plugin_path=`cd $plugin_path; pwd;` 37 | 38 | n=${#checkdirs[@]} 39 | i=0 40 | while [ "$i" -lt "$n" ]; do 41 | dir="$phpbb_path/${checkdirs[$i]}" 42 | if [ ! -e "$dir" ]; then 43 | echo "Could not find $dir" 44 | print_help 45 | fi 46 | dir="$absolute_plugin_path/${checkdirs[$i]}" 47 | if [ ! -e "$dir" ]; then 48 | echo "Could not find $dir" 49 | print_help 50 | fi 51 | let "i = $i + 1" 52 | done 53 | 54 | n=${#files[@]} 55 | i=0 56 | while [ "$i" -lt "$n" ]; do 57 | file="${files[$i]}" 58 | ln -vs "$absolute_plugin_path/$file" "$phpbb_path/$file" 59 | let "i = $i + 1" 60 | done -------------------------------------------------------------------------------- /sphinx_plugin/config/sphinx_stopwords.txt: -------------------------------------------------------------------------------- 1 | a 2 | about 3 | after 4 | ago 5 | all 6 | almost 7 | along 8 | alot 9 | also 10 | am 11 | an 12 | and 13 | answer 14 | any 15 | anybody 16 | anybodys 17 | anywhere 18 | are 19 | arent 20 | around 21 | as 22 | ask 23 | askd 24 | at 25 | bad 26 | be 27 | because 28 | been 29 | before 30 | being 31 | best 32 | better 33 | between 34 | big 35 | btw 36 | but 37 | by 38 | can 39 | cant 40 | come 41 | could 42 | couldnt 43 | day 44 | days 45 | days 46 | did 47 | didnt 48 | do 49 | does 50 | doesnt 51 | dont 52 | down 53 | each 54 | etc 55 | either 56 | else 57 | even 58 | ever 59 | every 60 | everybody 61 | everybodys 62 | everyone 63 | far 64 | find 65 | for 66 | found 67 | from 68 | get 69 | go 70 | going 71 | gone 72 | good 73 | got 74 | gotten 75 | had 76 | has 77 | have 78 | havent 79 | having 80 | her 81 | here 82 | hers 83 | him 84 | his 85 | home 86 | how 87 | hows 88 | href 89 | I 90 | Ive 91 | if 92 | in 93 | ini 94 | into 95 | is 96 | isnt 97 | it 98 | its 99 | its 100 | just 101 | know 102 | large 103 | less 104 | like 105 | liked 106 | little 107 | looking 108 | look 109 | looked 110 | looking 111 | lot 112 | maybe 113 | many 114 | me 115 | more 116 | most 117 | much 118 | must 119 | mustnt 120 | my 121 | near 122 | need 123 | never 124 | new 125 | news 126 | no 127 | none 128 | not 129 | nothing 130 | now 131 | of 132 | off 133 | often 134 | old 135 | on 136 | once 137 | only 138 | oops 139 | or 140 | other 141 | our 142 | ours 143 | out 144 | over 145 | page 146 | please 147 | put 148 | question 149 | questions 150 | questioned 151 | quote 152 | rather 153 | really 154 | recent 155 | said 156 | saw 157 | say 158 | says 159 | she 160 | see 161 | sees 162 | should 163 | sites 164 | small 165 | so 166 | some 167 | something 168 | sometime 169 | somewhere 170 | soon 171 | take 172 | than 173 | true 174 | thank 175 | that 176 | thatd 177 | thats 178 | the 179 | their 180 | theirs 181 | theres 182 | theirs 183 | them 184 | then 185 | there 186 | these 187 | they 188 | theyll 189 | theyd 190 | theyre 191 | this 192 | those 193 | though 194 | through 195 | thus 196 | time 197 | times 198 | to 199 | too 200 | under 201 | until 202 | untrue 203 | up 204 | upon 205 | use 206 | users 207 | version 208 | very 209 | via 210 | want 211 | was 212 | way 213 | we 214 | well 215 | went 216 | were 217 | werent 218 | what 219 | when 220 | where 221 | which 222 | who 223 | whom 224 | whose 225 | why 226 | wide 227 | will 228 | with 229 | within 230 | without 231 | wont 232 | world 233 | worse 234 | worst 235 | would 236 | wrote 237 | www 238 | yes 239 | yet 240 | you 241 | youd 242 | youll 243 | your 244 | youre 245 | yours 246 | AFAIK 247 | IIRC 248 | LOL 249 | ROTF 250 | ROTFLMAO 251 | YMMV -------------------------------------------------------------------------------- /sphinx_mod/install.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 |
7 | http://opensource.org/licenses/gpl-license.php GNU General Public License v2 8 | 9 | Sphinx4phpBB 10 | 11 | This MOD provides a Sphinx search backend plugin for phpBB. It allows phpBB to search posts through a Sphinx installation. See http://www.sphinxsearch.com for more details on Sphinx fulltext search. 12 | 13 | 14 | Sphinx (v0.9.8[.1]) has to be installed for this plugin to work. 15 | 16 | You do NOT need to configure Sphinx. All configuration is handled through the phpBB ACP interface of the plugin. 17 | 18 | To UPDATE from previous versions simply replace the files with the new ones. Then enter the ACP. Delete the Sphinx index under MAINTENANCE -> DATABASE -> Search index. Go to GENERAL -> SERVER CONFIGURATION -> Search Settings. Submit the page once - even if you made no changes. Then go to MAINTENANCE -> DATABASE -> Search index and create a new index for Sphinx. 19 | 20 | 21 | 22 | 23 | Nils Adermann 24 | naderman 25 | http://www.naderman.de 26 | 27 | 28 | 29 | 1.0.beta2 30 | 31 | 32 | easy 33 | 34 | 3.0.2 35 | 36 | 37 | 38 | 39 | 2008-11-07 40 | 1.0.beta2 41 | 42 | check log/write_test not logwrite_test 43 | restart searchd after config change 44 | max matches = 200000 45 | avoid a NOTICE on failed connection 46 | sphinx does not handle index updates through attribute updates ... 47 | make sure the values are integers and not strings 48 | sort topics by last_post_time, retry connecting on connection refused 49 | sort groups rather than elements of groups 50 | retry connecting multiple times 51 | locate pidof rather than assume it is in path 52 | allow choosing the indexer memory limit in the ACP 53 | 54 | 55 | 56 | 2008-11-01 57 | 1.0.beta1 58 | 59 | Initial public release 60 | 61 | 62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Go to the ACP. 76 | Configure and enable the plugin under GENERAL -> SERVER CONFIGURATION -> Search Settings. 77 | Then create an index under MAINTENANCE -> DATABASE -> Search index. 78 | 79 | This package also comes with an optional english stopwords file in the config folder. See the ACP for instructions on what to do with it. 80 | 81 | 82 | 83 |
84 | -------------------------------------------------------------------------------- /sphinx_plugin/language/en/mods/fulltext_sphinx.php: -------------------------------------------------------------------------------- 1 | 'Automatically configure Sphinx', 35 | 'FULLTEXT_SPHINX_AUTOCONF_EXPLAIN' => 'This is the easiest way to install Sphinx, just select the settings here and a config file will be written for you. This requires write permissions on the configuration folder.', 36 | 'FULLTEXT_SPHINX_AUTORUN' => 'Automatically run Sphinx', 37 | 'FULLTEXT_SPHINX_AUTORUN_EXPLAIN' => 'This is the easiest way to run Sphinx. Select the paths in this dialogue and the Sphinx daemon will be started and stopped as needed. You can also create an index from the ACP. If your PHP installation forbids the use of exec you can disable this and run Sphinx manually.', 38 | 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', 39 | 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', 40 | 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', 41 | 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', 42 | 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', 43 | 'FULLTEXT_SPHINX_CONFIGURE_BEFORE' => 'Configure the following settings BEFORE activating Sphinx', 44 | 'FULLTEXT_SPHINX_CONFIGURE_AFTER' => 'The following settings do not have to be configured before activating Sphinx', 45 | 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 46 | 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', 47 | 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', 48 | 'FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND' => 'The directory %s does not exist. Please correct your path settings.', 49 | 'FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE' => 'The file %s is not executable for the webserver.', 50 | 'FULLTEXT_SPHINX_FILE_NOT_FOUND' => 'The file %s does not exist. Please correct your path settings.', 51 | 'FULLTEXT_SPHINX_FILE_NOT_WRITABLE' => 'The file %s cannot be written by the webserver.', 52 | 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 53 | 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 54 | 'FULLTEXT_SPHINX_LAST_SEARCHES' => 'Recent search queries', 55 | 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 56 | 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', 57 | 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', 58 | 'FULLTEXT_SPHINX_REQUIRES_EXEC' => 'The sphinx plugin for phpBB requires PHP’s exec function which is disabled on your system.', 59 | 'FULLTEXT_SPHINX_UNCONFIGURED' => 'Please set all necessary options in the "Fulltext Sphinx" section of the previous page before you try to activate the sphinx plugin.', 60 | 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', 61 | 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', 62 | 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', 63 | )); 64 | 65 | ?> -------------------------------------------------------------------------------- /sphinx_plugin/includes/functions_sphinx.php: -------------------------------------------------------------------------------- 1 | read($filename); 39 | } 40 | } 41 | 42 | /** 43 | * Get a section object by its name 44 | * 45 | * @param string $name The name of the section that shall be returned 46 | * @return sphinx_config_section The section object or null if none was found 47 | */ 48 | function &get_section_by_name($name) 49 | { 50 | for ($i = 0, $n = sizeof($this->sections); $i < $n; $i++) 51 | { 52 | // make sure this is really a section object and not a comment 53 | if (is_a($this->sections[$i], 'sphinx_config_section') && $this->sections[$i]->get_name() == $name) 54 | { 55 | return $this->sections[$i]; 56 | } 57 | } 58 | $null = null; 59 | return $null; 60 | } 61 | 62 | /** 63 | * Appends a new empty section to the end of the config 64 | * 65 | * @param string $name The name for the new section 66 | * @return sphinx_config_section The newly created section object 67 | */ 68 | function &add_section($name) 69 | { 70 | $this->sections[] = new sphinx_config_section($name, ''); 71 | return $this->sections[sizeof($this->sections) - 1]; 72 | } 73 | 74 | /** 75 | * Parses the config file at the given path, which is stored in $this->loaded for later use 76 | * 77 | * @param string $filename The path to the config file 78 | */ 79 | function read($filename) 80 | { 81 | // split the file into lines, we'll process it line by line 82 | $config_file = file($filename); 83 | 84 | $this->sections = array(); 85 | 86 | $section = null; 87 | $found_opening_bracket = false; 88 | $in_value = false; 89 | 90 | foreach ($config_file as $i => $line) 91 | { 92 | // if the value of a variable continues to the next line because the line break was escaped 93 | // then we don't trim leading space but treat it as a part of the value 94 | if ($in_value) 95 | { 96 | $line = rtrim($line); 97 | } 98 | else 99 | { 100 | $line = trim($line); 101 | } 102 | 103 | // if we're not inside a section look for one 104 | if (!$section) 105 | { 106 | // add empty lines and comments as comment objects to the section list 107 | // that way they're not deleted when reassembling the file from the sections 108 | if (!$line || $line[0] == '#') 109 | { 110 | $this->sections[] = new sphinx_config_comment($config_file[$i]); 111 | continue; 112 | } 113 | else 114 | { 115 | // otherwise we scan the line reading the section name until we find 116 | // an opening curly bracket or a comment 117 | $section_name = ''; 118 | $section_name_comment = ''; 119 | $found_opening_bracket = false; 120 | for ($j = 0, $n = strlen($line); $j < $n; $j++) 121 | { 122 | if ($line[$j] == '#') 123 | { 124 | $section_name_comment = substr($line, $j); 125 | break; 126 | } 127 | 128 | if ($found_opening_bracket) 129 | { 130 | continue; 131 | } 132 | 133 | if ($line[$j] == '{') 134 | { 135 | $found_opening_bracket = true; 136 | continue; 137 | } 138 | 139 | $section_name .= $line[$j]; 140 | } 141 | 142 | // and then we create the new section object 143 | $section_name = trim($section_name); 144 | $section = new sphinx_config_section($section_name, $section_name_comment); 145 | } 146 | } 147 | else // if we're looking for variables inside a section 148 | { 149 | $skip_first = false; 150 | 151 | // if we're not in a value continuing over the line feed 152 | if (!$in_value) 153 | { 154 | // then add empty lines and comments as comment objects to the variable list 155 | // of this section so they're not deleted on reassembly 156 | if (!$line || $line[0] == '#') 157 | { 158 | $section->add_variable(new sphinx_config_comment($config_file[$i])); 159 | continue; 160 | } 161 | 162 | // as long as we haven't yet actually found an opening bracket for this section 163 | // we treat everything as comments so it's not deleted either 164 | if (!$found_opening_bracket) 165 | { 166 | if ($line[0] == '{') 167 | { 168 | $skip_first = true; 169 | $line = substr($line, 1); 170 | $found_opening_bracket = true; 171 | } 172 | else 173 | { 174 | $section->add_variable(new sphinx_config_comment($config_file[$i])); 175 | continue; 176 | } 177 | } 178 | } 179 | 180 | // if we did not find a comment in this line or still add to the previous line's value ... 181 | if ($line || $in_value) 182 | { 183 | if (!$in_value) 184 | { 185 | $name = ''; 186 | $value = ''; 187 | $comment = ''; 188 | $found_assignment = false; 189 | } 190 | $in_value = false; 191 | $end_section = false; 192 | 193 | // ... then we should prase this line char by char: 194 | // - first there's the variable name 195 | // - then an equal sign 196 | // - the variable value 197 | // - possibly a backslash before the linefeed in this case we need to continue 198 | // parsing the value in the next line 199 | // - a # indicating that the rest of the line is a comment 200 | // - a closing curly bracket indicating the end of this section 201 | for ($j = 0, $n = strlen($line); $j < $n; $j++) 202 | { 203 | if ($line[$j] == '#') 204 | { 205 | $comment = substr($line, $j); 206 | break; 207 | } 208 | else if ($line[$j] == '}') 209 | { 210 | $comment = substr($line, $j + 1); 211 | $end_section = true; 212 | break; 213 | } 214 | else if (!$found_assignment) 215 | { 216 | if ($line[$j] == '=') 217 | { 218 | $found_assignment = true; 219 | } 220 | else 221 | { 222 | $name .= $line[$j]; 223 | } 224 | } 225 | else 226 | { 227 | if ($line[$j] == '\\' && $j == $n - 1) 228 | { 229 | $value .= "\n"; 230 | $in_value = true; 231 | continue 2; // go to the next line and keep processing the value in there 232 | } 233 | $value .= $line[$j]; 234 | } 235 | } 236 | 237 | // if a name and an equal sign were found then we have append a new variable object to the section 238 | if ($name && $found_assignment) 239 | { 240 | $section->add_variable(new sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); 241 | continue; 242 | } 243 | 244 | // if we found a closing curly bracket this section has been completed and we can append it to the section list 245 | // and continue with looking for the next section 246 | if ($end_section) 247 | { 248 | $section->set_end_comment($comment); 249 | $this->sections[] = $section; 250 | $section = null; 251 | continue; 252 | } 253 | } 254 | 255 | // if we did not find anything meaningful up to here, then just treat it as a comment 256 | $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; 257 | $section->add_variable(new sphinx_config_comment($comment)); 258 | } 259 | } 260 | 261 | // keep the filename for later use 262 | $this->loaded = $filename; 263 | } 264 | 265 | /** 266 | * Writes the config data into a file 267 | * 268 | * @param string $filename The optional filename into which the config data shall be written. 269 | * If it's not specified it will be written into the file that the config 270 | * was originally read from. 271 | */ 272 | function write($filename = false) 273 | { 274 | if ($filename === false && $this->loaded) 275 | { 276 | $filename = $this->loaded; 277 | } 278 | 279 | $data = ""; 280 | foreach ($this->sections as $section) 281 | { 282 | $data .= $section->to_string(); 283 | } 284 | 285 | $fp = fopen($filename, 'wb'); 286 | fwrite($fp, $data); 287 | fclose($fp); 288 | } 289 | } 290 | 291 | /** 292 | * sphinx_config_section 293 | * Represents a single section inside the sphinx configuration 294 | */ 295 | class sphinx_config_section 296 | { 297 | var $name; 298 | var $comment; 299 | var $end_comment; 300 | var $variables = array(); 301 | 302 | /** 303 | * Construct a new section 304 | * 305 | * @param string $name Name of the section 306 | * @param string $comment Comment that should be appended after the name in the 307 | * textual format. 308 | */ 309 | function sphinx_config_section($name, $comment) 310 | { 311 | $this->name = $name; 312 | $this->comment = $comment; 313 | $this->end_comment = ''; 314 | } 315 | 316 | /** 317 | * Add a variable object to the list of variables in this section 318 | * 319 | * @param sphinx_config_variable $variable The variable object 320 | */ 321 | function add_variable($variable) 322 | { 323 | $this->variables[] = $variable; 324 | } 325 | 326 | /** 327 | * Adds a comment after the closing bracket in the textual representation 328 | */ 329 | function set_end_comment($end_comment) 330 | { 331 | $this->end_comment = $end_comment; 332 | } 333 | 334 | /** 335 | * Getter for the name of this section 336 | * 337 | * @return string Section's name 338 | */ 339 | function get_name() 340 | { 341 | return $this->name; 342 | } 343 | 344 | /** 345 | * Get a variable object by its name 346 | * 347 | * @param string $name The name of the variable that shall be returned 348 | * @return sphinx_config_section The first variable object from this section with the 349 | * given name or null if none was found 350 | */ 351 | function &get_variable_by_name($name) 352 | { 353 | for ($i = 0, $n = sizeof($this->variables); $i < $n; $i++) 354 | { 355 | // make sure this is a variable object and not a comment 356 | if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) 357 | { 358 | return $this->variables[$i]; 359 | } 360 | } 361 | $null = null; 362 | return $null; 363 | } 364 | 365 | /** 366 | * Deletes all variables with the given name 367 | * 368 | * @param string $name The name of the variable objects that are supposed to be removed 369 | */ 370 | function delete_variables_by_name($name) 371 | { 372 | for ($i = 0; $i < sizeof($this->variables); $i++) 373 | { 374 | // make sure this is a variable object and not a comment 375 | if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) 376 | { 377 | array_splice($this->variables, $i, 1); 378 | $i--; 379 | } 380 | } 381 | } 382 | 383 | /** 384 | * Create a new variable object and append it to the variable list of this section 385 | * 386 | * @param string $name The name for the new variable 387 | * @param string $value The value for the new variable 388 | * @return sphinx_config_variable Variable object that was created 389 | */ 390 | function &create_variable($name, $value) 391 | { 392 | $this->variables[] = new sphinx_config_variable($name, $value, ''); 393 | return $this->variables[sizeof($this->variables) - 1]; 394 | } 395 | 396 | /** 397 | * Turns this object into a string which can be written to a config file 398 | * 399 | * @return string Config data in textual form, parsable for sphinx 400 | */ 401 | function to_string() 402 | { 403 | $content = $this->name . " " . $this->comment . "\n{\n"; 404 | 405 | // make sure we don't get too many newlines after the opening bracket 406 | while (trim($this->variables[0]->to_string()) == "") 407 | { 408 | array_shift($this->variables); 409 | } 410 | 411 | foreach ($this->variables as $variable) 412 | { 413 | $content .= $variable->to_string(); 414 | } 415 | $content .= '}' . $this->end_comment . "\n"; 416 | 417 | return $content; 418 | } 419 | } 420 | 421 | /** 422 | * sphinx_config_variable 423 | * Represents a single variable inside the sphinx configuration 424 | */ 425 | class sphinx_config_variable 426 | { 427 | var $name; 428 | var $value; 429 | var $comment; 430 | 431 | /** 432 | * Constructs a new variable object 433 | * 434 | * @param string $name Name of the variable 435 | * @param string $value Value of the variable 436 | * @param string $comment Optional comment after the variable in the 437 | * config file 438 | */ 439 | function sphinx_config_variable($name, $value, $comment) 440 | { 441 | $this->name = $name; 442 | $this->value = $value; 443 | $this->comment = $comment; 444 | } 445 | 446 | /** 447 | * Getter for the variable's name 448 | * 449 | * @return string The variable object's name 450 | */ 451 | function get_name() 452 | { 453 | return $this->name; 454 | } 455 | 456 | /** 457 | * Allows changing the variable's value 458 | * 459 | * @param string $value New value for this variable 460 | */ 461 | function set_value($value) 462 | { 463 | $this->value = $value; 464 | } 465 | 466 | /** 467 | * Turns this object into a string readable by sphinx 468 | * 469 | * @return string Config data in textual form 470 | */ 471 | function to_string() 472 | { 473 | return "\t" . $this->name . ' = ' . str_replace("\n", "\\\n", $this->value) . ' ' . $this->comment . "\n"; 474 | } 475 | } 476 | 477 | 478 | /** 479 | * sphinx_config_comment 480 | * Represents a comment inside the sphinx configuration 481 | */ 482 | class sphinx_config_comment 483 | { 484 | var $exact_string; 485 | 486 | /** 487 | * Create a new comment 488 | * 489 | * @param string $exact_string The content of the comment including newlines, leading whitespace, etc. 490 | */ 491 | function sphinx_config_comment($exact_string) 492 | { 493 | $this->exact_string = $exact_string; 494 | } 495 | 496 | /** 497 | * Simply returns the comment as it was created 498 | * 499 | * @return string The exact string that was specified in the constructor 500 | */ 501 | function to_string() 502 | { 503 | return $this->exact_string; 504 | } 505 | } 506 | 507 | ?> -------------------------------------------------------------------------------- /sphinx_mod/license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /sphinx_plugin/includes/sphinxapi-0.9.8.php: -------------------------------------------------------------------------------- 1 | =8 ) 90 | { 91 | $i = (int)$v; 92 | return pack ( "NN", $i>>32, $i&((1<<32)-1) ); 93 | } 94 | 95 | // x32 route, bcmath 96 | $x = "4294967296"; 97 | if ( function_exists("bcmul") ) 98 | { 99 | $h = bcdiv ( $v, $x, 0 ); 100 | $l = bcmod ( $v, $x ); 101 | return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit 102 | } 103 | 104 | // x32 route, 15 or less decimal digits 105 | // we can use float, because its actually double and has 52 precision bits 106 | if ( strlen($v)<=15 ) 107 | { 108 | $f = (float)$v; 109 | $h = (int)($f/$x); 110 | $l = (int)($f-$x*$h); 111 | return pack ( "NN", $h, $l ); 112 | } 113 | 114 | // x32 route, 16 or more decimal digits 115 | // well, let me know if you *really* need this 116 | die ( "INTERNAL ERROR: packing more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); 117 | } 118 | 119 | 120 | /// portably unpack 64 unsigned bits, network order to numeric 121 | function sphUnpack64 ( $v ) 122 | { 123 | list($h,$l) = array_values ( unpack ( "N*N*", $v ) ); 124 | 125 | // x64 route 126 | if ( PHP_INT_SIZE>=8 ) 127 | { 128 | if ( $h<0 ) $h += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again 129 | if ( $l<0 ) $l += (1<<32); 130 | return ($h<<32) + $l; 131 | } 132 | 133 | // x32 route 134 | $h = sprintf ( "%u", $h ); 135 | $l = sprintf ( "%u", $l ); 136 | $x = "4294967296"; 137 | 138 | // bcmath 139 | if ( function_exists("bcmul") ) 140 | return bcadd ( $l, bcmul ( $x, $h ) ); 141 | 142 | // no bcmath, 15 or less decimal digits 143 | // we can use float, because its actually double and has 52 precision bits 144 | if ( $h<1048576 ) 145 | { 146 | $f = ((float)$h)*$x + (float)$l; 147 | return sprintf ( "%.0f", $f ); // builtin conversion is only about 39-40 bits precise! 148 | } 149 | 150 | // x32 route, 16 or more decimal digits 151 | // well, let me know if you *really* need this 152 | die ( "INTERNAL ERROR: unpacking more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); 153 | } 154 | 155 | 156 | /// sphinx searchd client class 157 | class SphinxClient 158 | { 159 | var $_host; ///< searchd host (default is "localhost") 160 | var $_port; ///< searchd port (default is 3312) 161 | var $_offset; ///< how many records to seek from result-set start (default is 0) 162 | var $_limit; ///< how many records to return from result-set starting at offset (default is 20) 163 | var $_mode; ///< query matching mode (default is SPH_MATCH_ALL) 164 | var $_weights; ///< per-field weights (default is 1 for all fields) 165 | var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE) 166 | var $_sortby; ///< attribute to sort by (defualt is "") 167 | var $_min_id; ///< min ID to match (default is 0, which means no limit) 168 | var $_max_id; ///< max ID to match (default is 0, which means no limit) 169 | var $_filters; ///< search filters 170 | var $_groupby; ///< group-by attribute name 171 | var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with) 172 | var $_groupsort; ///< group-by sorting clause (to sort groups in result set with) 173 | var $_groupdistinct;///< group-by count-distinct attribute 174 | var $_maxmatches; ///< max matches to retrieve 175 | var $_cutoff; ///< cutoff to stop searching at (default is 0) 176 | var $_retrycount; ///< distributed retries count 177 | var $_retrydelay; ///< distributed retries delay 178 | var $_anchor; ///< geographical anchor point 179 | var $_indexweights; ///< per-index weights 180 | var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25) 181 | var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit) 182 | var $_fieldweights; ///< per-field-name weights 183 | 184 | var $_error; ///< last error message 185 | var $_warning; ///< last warning message 186 | 187 | var $_reqs; ///< requests array for multi-query 188 | var $_mbenc; ///< stored mbstring encoding 189 | var $_arrayresult; ///< whether $result["matches"] should be a hash or an array 190 | var $_timeout; ///< connect timeout 191 | 192 | ///////////////////////////////////////////////////////////////////////////// 193 | // common stuff 194 | ///////////////////////////////////////////////////////////////////////////// 195 | 196 | /// create a new client object and fill defaults 197 | function SphinxClient () 198 | { 199 | // per-client-object settings 200 | $this->_host = "localhost"; 201 | $this->_port = 3312; 202 | 203 | // per-query settings 204 | $this->_offset = 0; 205 | $this->_limit = 20; 206 | $this->_mode = SPH_MATCH_ALL; 207 | $this->_weights = array (); 208 | $this->_sort = SPH_SORT_RELEVANCE; 209 | $this->_sortby = ""; 210 | $this->_min_id = 0; 211 | $this->_max_id = 0; 212 | $this->_filters = array (); 213 | $this->_groupby = ""; 214 | $this->_groupfunc = SPH_GROUPBY_DAY; 215 | $this->_groupsort = "@group desc"; 216 | $this->_groupdistinct= ""; 217 | $this->_maxmatches = 1000; 218 | $this->_cutoff = 0; 219 | $this->_retrycount = 0; 220 | $this->_retrydelay = 0; 221 | $this->_anchor = array (); 222 | $this->_indexweights= array (); 223 | $this->_ranker = SPH_RANK_PROXIMITY_BM25; 224 | $this->_maxquerytime= 0; 225 | $this->_fieldweights= array(); 226 | 227 | $this->_error = ""; // per-reply fields (for single-query case) 228 | $this->_warning = ""; 229 | $this->_reqs = array (); // requests storage (for multi-query case) 230 | $this->_mbenc = ""; 231 | $this->_arrayresult = false; 232 | $this->_timeout = 0; 233 | } 234 | 235 | /// get last error message (string) 236 | function GetLastError () 237 | { 238 | return $this->_error; 239 | } 240 | 241 | /// get last warning message (string) 242 | function GetLastWarning () 243 | { 244 | return $this->_warning; 245 | } 246 | 247 | /// set searchd host name (string) and port (integer) 248 | function SetServer ( $host, $port ) 249 | { 250 | assert ( is_string($host) ); 251 | assert ( is_int($port) ); 252 | $this->_host = $host; 253 | $this->_port = $port; 254 | } 255 | 256 | /// set server connection timeout (0 to remove) 257 | function SetConnectTimeout ( $timeout ) 258 | { 259 | assert ( is_numeric($timeout) ); 260 | $this->_timeout = $timeout; 261 | } 262 | 263 | ///////////////////////////////////////////////////////////////////////////// 264 | 265 | /// enter mbstring workaround mode 266 | function _MBPush () 267 | { 268 | $this->_mbenc = ""; 269 | if ( ini_get ( "mbstring.func_overload" ) & 2 ) 270 | { 271 | $this->_mbenc = mb_internal_encoding(); 272 | mb_internal_encoding ( "latin1" ); 273 | } 274 | } 275 | 276 | /// leave mbstring workaround mode 277 | function _MBPop () 278 | { 279 | if ( $this->_mbenc ) 280 | mb_internal_encoding ( $this->_mbenc ); 281 | } 282 | 283 | /// connect to searchd server 284 | function _Connect ($allow_retry = true) 285 | { 286 | $errno = 0; 287 | $errstr = ""; 288 | if ( $this->_timeout<=0 ) 289 | $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr ); 290 | else 291 | $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr, $this->_timeout ); 292 | 293 | if ( !$fp ) 294 | { 295 | $errstr = trim ( $errstr ); 296 | $this->_error = "connection to {$this->_host}:{$this->_port} failed (errno=$errno, msg=$errstr)"; 297 | return false; 298 | } 299 | 300 | // check version 301 | //list(,$v) = unpack ( "N*", fread ( $fp, 4 ) ); 302 | $version_data = unpack ( "N*", fread ( $fp, 4 ) ); 303 | if (!isset($version_data[1])) 304 | { 305 | // this should not happen, try to reconnect ONCE 306 | if ($allow_retry) 307 | { 308 | return $this->_Connect(false); 309 | } 310 | else 311 | { 312 | $this->_error = "unexpected version data"; 313 | return false; 314 | } 315 | } 316 | $v = $version_data[1]; 317 | $v = (int)$v; 318 | if ( $v<1 ) 319 | { 320 | fclose ( $fp ); 321 | $this->_error = "expected searchd protocol version 1+, got version '$v'"; 322 | return false; 323 | } 324 | 325 | // all ok, send my version 326 | fwrite ( $fp, pack ( "N", 1 ) ); 327 | return $fp; 328 | } 329 | 330 | /// get and check response packet from searchd server 331 | function _GetResponse ( $fp, $client_ver ) 332 | { 333 | $response = ""; 334 | $len = 0; 335 | 336 | $header = fread ( $fp, 8 ); 337 | if ( strlen($header)==8 ) 338 | { 339 | list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) ); 340 | $left = $len; 341 | while ( $left>0 && !feof($fp) ) 342 | { 343 | $chunk = fread ( $fp, $left ); 344 | if ( $chunk ) 345 | { 346 | $response .= $chunk; 347 | $left -= strlen($chunk); 348 | } 349 | } 350 | } 351 | fclose ( $fp ); 352 | 353 | // check response 354 | $read = strlen ( $response ); 355 | if ( !$response || $read!=$len ) 356 | { 357 | $this->_error = $len 358 | ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)" 359 | : "received zero-sized searchd response"; 360 | return false; 361 | } 362 | 363 | // check status 364 | if ( $status==SEARCHD_WARNING ) 365 | { 366 | list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) ); 367 | $this->_warning = substr ( $response, 4, $wlen ); 368 | return substr ( $response, 4+$wlen ); 369 | } 370 | if ( $status==SEARCHD_ERROR ) 371 | { 372 | $this->_error = "searchd error: " . substr ( $response, 4 ); 373 | return false; 374 | } 375 | if ( $status==SEARCHD_RETRY ) 376 | { 377 | $this->_error = "temporary searchd error: " . substr ( $response, 4 ); 378 | return false; 379 | } 380 | if ( $status!=SEARCHD_OK ) 381 | { 382 | $this->_error = "unknown status code '$status'"; 383 | return false; 384 | } 385 | 386 | // check version 387 | if ( $ver<$client_ver ) 388 | { 389 | $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work", 390 | $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ); 391 | } 392 | 393 | return $response; 394 | } 395 | 396 | ///////////////////////////////////////////////////////////////////////////// 397 | // searching 398 | ///////////////////////////////////////////////////////////////////////////// 399 | 400 | /// set offset and count into result set, 401 | /// and optionally set max-matches and cutoff limits 402 | function SetLimits ( $offset, $limit, $max=0, $cutoff=0 ) 403 | { 404 | assert ( is_int($offset) ); 405 | assert ( is_int($limit) ); 406 | assert ( $offset>=0 ); 407 | assert ( $limit>0 ); 408 | assert ( $max>=0 ); 409 | $this->_offset = $offset; 410 | $this->_limit = $limit; 411 | if ( $max>0 ) 412 | $this->_maxmatches = $max; 413 | if ( $cutoff>0 ) 414 | $this->_cutoff = $cutoff; 415 | } 416 | 417 | /// set maximum query time, in milliseconds, per-index 418 | /// integer, 0 means "do not limit" 419 | function SetMaxQueryTime ( $max ) 420 | { 421 | assert ( is_int($max) ); 422 | assert ( $max>=0 ); 423 | $this->_maxquerytime = $max; 424 | } 425 | 426 | /// set matching mode 427 | function SetMatchMode ( $mode ) 428 | { 429 | assert ( $mode==SPH_MATCH_ALL 430 | || $mode==SPH_MATCH_ANY 431 | || $mode==SPH_MATCH_PHRASE 432 | || $mode==SPH_MATCH_BOOLEAN 433 | || $mode==SPH_MATCH_EXTENDED 434 | || $mode==SPH_MATCH_FULLSCAN 435 | || $mode==SPH_MATCH_EXTENDED2 ); 436 | $this->_mode = $mode; 437 | } 438 | 439 | /// set ranking mode 440 | function SetRankingMode ( $ranker ) 441 | { 442 | assert ( $ranker==SPH_RANK_PROXIMITY_BM25 443 | || $ranker==SPH_RANK_BM25 444 | || $ranker==SPH_RANK_NONE 445 | || $ranker==SPH_RANK_WORDCOUNT ); 446 | $this->_ranker = $ranker; 447 | } 448 | 449 | /// set matches sorting mode 450 | function SetSortMode ( $mode, $sortby="" ) 451 | { 452 | assert ( 453 | $mode==SPH_SORT_RELEVANCE || 454 | $mode==SPH_SORT_ATTR_DESC || 455 | $mode==SPH_SORT_ATTR_ASC || 456 | $mode==SPH_SORT_TIME_SEGMENTS || 457 | $mode==SPH_SORT_EXTENDED || 458 | $mode==SPH_SORT_EXPR ); 459 | assert ( is_string($sortby) ); 460 | assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 ); 461 | 462 | $this->_sort = $mode; 463 | $this->_sortby = $sortby; 464 | } 465 | 466 | /// bind per-field weights by order 467 | /// DEPRECATED; use SetFieldWeights() instead 468 | function SetWeights ( $weights ) 469 | { 470 | assert ( is_array($weights) ); 471 | foreach ( $weights as $weight ) 472 | assert ( is_int($weight) ); 473 | 474 | $this->_weights = $weights; 475 | } 476 | 477 | /// bind per-field weights by name 478 | function SetFieldWeights ( $weights ) 479 | { 480 | assert ( is_array($weights) ); 481 | foreach ( $weights as $name=>$weight ) 482 | { 483 | assert ( is_string($name) ); 484 | assert ( is_int($weight) ); 485 | } 486 | $this->_fieldweights = $weights; 487 | } 488 | 489 | /// bind per-index weights by name 490 | function SetIndexWeights ( $weights ) 491 | { 492 | assert ( is_array($weights) ); 493 | foreach ( $weights as $index=>$weight ) 494 | { 495 | assert ( is_string($index) ); 496 | assert ( is_int($weight) ); 497 | } 498 | $this->_indexweights = $weights; 499 | } 500 | 501 | /// set IDs range to match 502 | /// only match records if document ID is beetwen $min and $max (inclusive) 503 | function SetIDRange ( $min, $max ) 504 | { 505 | assert ( is_numeric($min) ); 506 | assert ( is_numeric($max) ); 507 | assert ( $min<=$max ); 508 | $this->_min_id = $min; 509 | $this->_max_id = $max; 510 | } 511 | 512 | /// set values set filter 513 | /// only match records where $attribute value is in given set 514 | function SetFilter ( $attribute, $values, $exclude=false ) 515 | { 516 | assert ( is_string($attribute) ); 517 | assert ( is_array($values) ); 518 | assert ( count($values) ); 519 | 520 | if ( is_array($values) && count($values) ) 521 | { 522 | foreach ( $values as $value ) 523 | assert ( is_numeric($value) ); 524 | 525 | $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values ); 526 | } 527 | } 528 | 529 | /// set range filter 530 | /// only match records if $attribute value is beetwen $min and $max (inclusive) 531 | function SetFilterRange ( $attribute, $min, $max, $exclude=false ) 532 | { 533 | assert ( is_string($attribute) ); 534 | assert ( is_int($min) ); 535 | assert ( is_int($max) ); 536 | assert ( $min<=$max ); 537 | 538 | $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); 539 | } 540 | 541 | /// set float range filter 542 | /// only match records if $attribute value is beetwen $min and $max (inclusive) 543 | function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false ) 544 | { 545 | assert ( is_string($attribute) ); 546 | assert ( is_float($min) ); 547 | assert ( is_float($max) ); 548 | assert ( $min<=$max ); 549 | 550 | $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); 551 | } 552 | 553 | /// setup anchor point for geosphere distance calculations 554 | /// required to use @geodist in filters and sorting 555 | /// latitude and longitude must be in radians 556 | function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long ) 557 | { 558 | assert ( is_string($attrlat) ); 559 | assert ( is_string($attrlong) ); 560 | assert ( is_float($lat) ); 561 | assert ( is_float($long) ); 562 | 563 | $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long ); 564 | } 565 | 566 | /// set grouping attribute and function 567 | function SetGroupBy ( $attribute, $func, $groupsort="@group desc" ) 568 | { 569 | assert ( is_string($attribute) ); 570 | assert ( is_string($groupsort) ); 571 | assert ( $func==SPH_GROUPBY_DAY 572 | || $func==SPH_GROUPBY_WEEK 573 | || $func==SPH_GROUPBY_MONTH 574 | || $func==SPH_GROUPBY_YEAR 575 | || $func==SPH_GROUPBY_ATTR 576 | || $func==SPH_GROUPBY_ATTRPAIR ); 577 | 578 | $this->_groupby = $attribute; 579 | $this->_groupfunc = $func; 580 | $this->_groupsort = $groupsort; 581 | } 582 | 583 | /// set count-distinct attribute for group-by queries 584 | function SetGroupDistinct ( $attribute ) 585 | { 586 | assert ( is_string($attribute) ); 587 | $this->_groupdistinct = $attribute; 588 | } 589 | 590 | /// set distributed retries count and delay 591 | function SetRetries ( $count, $delay=0 ) 592 | { 593 | assert ( is_int($count) && $count>=0 ); 594 | assert ( is_int($delay) && $delay>=0 ); 595 | $this->_retrycount = $count; 596 | $this->_retrydelay = $delay; 597 | } 598 | 599 | /// set result set format (hash or array; hash by default) 600 | /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs 601 | function SetArrayResult ( $arrayresult ) 602 | { 603 | assert ( is_bool($arrayresult) ); 604 | $this->_arrayresult = $arrayresult; 605 | } 606 | 607 | ////////////////////////////////////////////////////////////////////////////// 608 | 609 | /// clear all filters (for multi-queries) 610 | function ResetFilters () 611 | { 612 | $this->_filters = array(); 613 | $this->_anchor = array(); 614 | } 615 | 616 | /// clear groupby settings (for multi-queries) 617 | function ResetGroupBy () 618 | { 619 | $this->_groupby = ""; 620 | $this->_groupfunc = SPH_GROUPBY_DAY; 621 | $this->_groupsort = "@group desc"; 622 | $this->_groupdistinct= ""; 623 | } 624 | 625 | ////////////////////////////////////////////////////////////////////////////// 626 | 627 | /// connect to searchd server, run given search query through given indexes, 628 | /// and return the search results 629 | function Query ( $query, $index="*", $comment="" ) 630 | { 631 | assert ( empty($this->_reqs) ); 632 | 633 | $this->AddQuery ( $query, $index, $comment ); 634 | $results = $this->RunQueries (); 635 | $this->_reqs = array (); // just in case it failed too early 636 | 637 | if ( !is_array($results) ) 638 | return false; // probably network error; error message should be already filled 639 | 640 | $this->_error = $results[0]["error"]; 641 | $this->_warning = $results[0]["warning"]; 642 | if ( $results[0]["status"]==SEARCHD_ERROR ) 643 | return false; 644 | else 645 | return $results[0]; 646 | } 647 | 648 | /// helper to pack floats in network byte order 649 | function _PackFloat ( $f ) 650 | { 651 | $t1 = pack ( "f", $f ); // machine order 652 | list(,$t2) = unpack ( "L*", $t1 ); // int in machine order 653 | return pack ( "N", $t2 ); 654 | } 655 | 656 | /// add query to multi-query batch 657 | /// returns index into results array from RunQueries() call 658 | function AddQuery ( $query, $index="*", $comment="" ) 659 | { 660 | // mbstring workaround 661 | $this->_MBPush (); 662 | 663 | // build request 664 | $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits 665 | $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby; 666 | $req .= pack ( "N", strlen($query) ) . $query; // query itself 667 | $req .= pack ( "N", count($this->_weights) ); // weights 668 | foreach ( $this->_weights as $weight ) 669 | $req .= pack ( "N", (int)$weight ); 670 | $req .= pack ( "N", strlen($index) ) . $index; // indexes 671 | $req .= pack ( "N", 1 ); // id64 range marker 672 | $req .= sphPack64 ( $this->_min_id ) . sphPack64 ( $this->_max_id ); // id64 range 673 | 674 | // filters 675 | $req .= pack ( "N", count($this->_filters) ); 676 | foreach ( $this->_filters as $filter ) 677 | { 678 | $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"]; 679 | $req .= pack ( "N", $filter["type"] ); 680 | switch ( $filter["type"] ) 681 | { 682 | case SPH_FILTER_VALUES: 683 | $req .= pack ( "N", count($filter["values"]) ); 684 | foreach ( $filter["values"] as $value ) 685 | $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms 686 | break; 687 | 688 | case SPH_FILTER_RANGE: 689 | $req .= pack ( "NN", $filter["min"], $filter["max"] ); 690 | break; 691 | 692 | case SPH_FILTER_FLOATRANGE: 693 | $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] ); 694 | break; 695 | 696 | default: 697 | assert ( 0 && "internal error: unhandled filter type" ); 698 | } 699 | $req .= pack ( "N", $filter["exclude"] ); 700 | } 701 | 702 | // group-by clause, max-matches count, group-sort clause, cutoff count 703 | $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby; 704 | $req .= pack ( "N", $this->_maxmatches ); 705 | $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort; 706 | $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay ); 707 | $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct; 708 | 709 | // anchor point 710 | if ( empty($this->_anchor) ) 711 | { 712 | $req .= pack ( "N", 0 ); 713 | } else 714 | { 715 | $a =& $this->_anchor; 716 | $req .= pack ( "N", 1 ); 717 | $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"]; 718 | $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"]; 719 | $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] ); 720 | } 721 | 722 | // per-index weights 723 | $req .= pack ( "N", count($this->_indexweights) ); 724 | foreach ( $this->_indexweights as $idx=>$weight ) 725 | $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight ); 726 | 727 | // max query time 728 | $req .= pack ( "N", $this->_maxquerytime ); 729 | 730 | // per-field weights 731 | $req .= pack ( "N", count($this->_fieldweights) ); 732 | foreach ( $this->_fieldweights as $field=>$weight ) 733 | $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight ); 734 | 735 | // comment 736 | $req .= pack ( "N", strlen($comment) ) . $comment; 737 | 738 | // mbstring workaround 739 | $this->_MBPop (); 740 | 741 | // store request to requests array 742 | $this->_reqs[] = $req; 743 | return count($this->_reqs)-1; 744 | } 745 | 746 | /// connect to searchd, run queries batch, and return an array of result sets 747 | function RunQueries () 748 | { 749 | if ( empty($this->_reqs) ) 750 | { 751 | $this->_error = "no queries defined, issue AddQuery() first"; 752 | return false; 753 | } 754 | 755 | // mbstring workaround 756 | $this->_MBPush (); 757 | 758 | if (!( $fp = $this->_Connect() )) 759 | { 760 | $this->_MBPop (); 761 | return false; 762 | } 763 | 764 | //////////////////////////// 765 | // send query, get response 766 | //////////////////////////// 767 | 768 | $nreqs = count($this->_reqs); 769 | $req = join ( "", $this->_reqs ); 770 | $len = 4+strlen($req); 771 | $req = pack ( "nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs ) . $req; // add header 772 | 773 | fwrite ( $fp, $req, $len+8 ); 774 | if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) )) 775 | { 776 | $this->_MBPop (); 777 | return false; 778 | } 779 | 780 | $this->_reqs = array (); 781 | 782 | ////////////////// 783 | // parse response 784 | ////////////////// 785 | 786 | $p = 0; // current position 787 | $max = strlen($response); // max position for checks, to protect against broken responses 788 | 789 | $results = array (); 790 | for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ ) 791 | { 792 | $results[] = array(); 793 | $result =& $results[$ires]; 794 | 795 | $result["error"] = ""; 796 | $result["warning"] = ""; 797 | 798 | // extract status 799 | list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 800 | $result["status"] = $status; 801 | if ( $status!=SEARCHD_OK ) 802 | { 803 | list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 804 | $message = substr ( $response, $p, $len ); $p += $len; 805 | 806 | if ( $status==SEARCHD_WARNING ) 807 | { 808 | $result["warning"] = $message; 809 | } else 810 | { 811 | $result["error"] = $message; 812 | continue; 813 | } 814 | } 815 | 816 | // read schema 817 | $fields = array (); 818 | $attrs = array (); 819 | 820 | list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 821 | while ( $nfields-->0 && $p<$max ) 822 | { 823 | list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 824 | $fields[] = substr ( $response, $p, $len ); $p += $len; 825 | } 826 | $result["fields"] = $fields; 827 | 828 | list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 829 | while ( $nattrs-->0 && $p<$max ) 830 | { 831 | list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 832 | $attr = substr ( $response, $p, $len ); $p += $len; 833 | list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 834 | $attrs[$attr] = $type; 835 | } 836 | $result["attrs"] = $attrs; 837 | 838 | // read match count 839 | list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 840 | list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 841 | 842 | // read matches 843 | $idx = -1; 844 | while ( $count-->0 && $p<$max ) 845 | { 846 | // index into result array 847 | $idx++; 848 | 849 | // parse document id and weight 850 | if ( $id64 ) 851 | { 852 | $doc = sphUnpack64 ( substr ( $response, $p, 8 ) ); $p += 8; 853 | list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 854 | } else 855 | { 856 | list ( $doc, $weight ) = array_values ( unpack ( "N*N*", 857 | substr ( $response, $p, 8 ) ) ); 858 | $p += 8; 859 | 860 | if ( PHP_INT_SIZE>=8 ) 861 | { 862 | // x64 route, workaround broken unpack() in 5.2.2+ 863 | if ( $doc<0 ) $doc += (1<<32); 864 | } else 865 | { 866 | // x32 route, workaround php signed/unsigned braindamage 867 | $doc = sprintf ( "%u", $doc ); 868 | } 869 | } 870 | $weight = sprintf ( "%u", $weight ); 871 | 872 | // create match entry 873 | if ( $this->_arrayresult ) 874 | $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight ); 875 | else 876 | $result["matches"][$doc]["weight"] = $weight; 877 | 878 | // parse and create attributes 879 | $attrvals = array (); 880 | foreach ( $attrs as $attr=>$type ) 881 | { 882 | // handle floats 883 | if ( $type==SPH_ATTR_FLOAT ) 884 | { 885 | list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 886 | list(,$fval) = unpack ( "f*", pack ( "L", $uval ) ); 887 | $attrvals[$attr] = $fval; 888 | continue; 889 | } 890 | 891 | // handle everything else as unsigned ints 892 | list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 893 | if ( $type & SPH_ATTR_MULTI ) 894 | { 895 | $attrvals[$attr] = array (); 896 | $nvalues = $val; 897 | while ( $nvalues-->0 && $p<$max ) 898 | { 899 | list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 900 | $attrvals[$attr][] = sprintf ( "%u", $val ); 901 | } 902 | } else 903 | { 904 | $attrvals[$attr] = sprintf ( "%u", $val ); 905 | } 906 | } 907 | 908 | if ( $this->_arrayresult ) 909 | $result["matches"][$idx]["attrs"] = $attrvals; 910 | else 911 | $result["matches"][$doc]["attrs"] = $attrvals; 912 | } 913 | 914 | list ( $total, $total_found, $msecs, $words ) = 915 | array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) ); 916 | $result["total"] = sprintf ( "%u", $total ); 917 | $result["total_found"] = sprintf ( "%u", $total_found ); 918 | $result["time"] = sprintf ( "%.3f", $msecs/1000 ); 919 | $p += 16; 920 | 921 | while ( $words-->0 && $p<$max ) 922 | { 923 | list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; 924 | $word = substr ( $response, $p, $len ); $p += $len; 925 | list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; 926 | $result["words"][$word] = array ( 927 | "docs"=>sprintf ( "%u", $docs ), 928 | "hits"=>sprintf ( "%u", $hits ) ); 929 | } 930 | } 931 | 932 | $this->_MBPop (); 933 | return $results; 934 | } 935 | 936 | ///////////////////////////////////////////////////////////////////////////// 937 | // excerpts generation 938 | ///////////////////////////////////////////////////////////////////////////// 939 | 940 | /// connect to searchd server, and generate exceprts (snippets) 941 | /// of given documents for given query. returns false on failure, 942 | /// an array of snippets on success 943 | function BuildExcerpts ( $docs, $index, $words, $opts=array() ) 944 | { 945 | assert ( is_array($docs) ); 946 | assert ( is_string($index) ); 947 | assert ( is_string($words) ); 948 | assert ( is_array($opts) ); 949 | 950 | $this->_MBPush (); 951 | 952 | if (!( $fp = $this->_Connect() )) 953 | { 954 | $this->_MBPop(); 955 | return false; 956 | } 957 | 958 | ///////////////// 959 | // fixup options 960 | ///////////////// 961 | 962 | if ( !isset($opts["before_match"]) ) $opts["before_match"] = ""; 963 | if ( !isset($opts["after_match"]) ) $opts["after_match"] = ""; 964 | if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... "; 965 | if ( !isset($opts["limit"]) ) $opts["limit"] = 256; 966 | if ( !isset($opts["around"]) ) $opts["around"] = 5; 967 | if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false; 968 | if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false; 969 | if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false; 970 | if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false; 971 | 972 | ///////////////// 973 | // build request 974 | ///////////////// 975 | 976 | // v.1.0 req 977 | $flags = 1; // remove spaces 978 | if ( $opts["exact_phrase"] ) $flags |= 2; 979 | if ( $opts["single_passage"] ) $flags |= 4; 980 | if ( $opts["use_boundaries"] ) $flags |= 8; 981 | if ( $opts["weight_order"] ) $flags |= 16; 982 | $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags 983 | $req .= pack ( "N", strlen($index) ) . $index; // req index 984 | $req .= pack ( "N", strlen($words) ) . $words; // req words 985 | 986 | // options 987 | $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"]; 988 | $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"]; 989 | $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"]; 990 | $req .= pack ( "N", (int)$opts["limit"] ); 991 | $req .= pack ( "N", (int)$opts["around"] ); 992 | 993 | // documents 994 | $req .= pack ( "N", count($docs) ); 995 | foreach ( $docs as $doc ) 996 | { 997 | assert ( is_string($doc) ); 998 | $req .= pack ( "N", strlen($doc) ) . $doc; 999 | } 1000 | 1001 | //////////////////////////// 1002 | // send query, get response 1003 | //////////////////////////// 1004 | 1005 | $len = strlen($req); 1006 | $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header 1007 | $wrote = fwrite ( $fp, $req, $len+8 ); 1008 | if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) )) 1009 | { 1010 | $this->_MBPop (); 1011 | return false; 1012 | } 1013 | 1014 | ////////////////// 1015 | // parse response 1016 | ////////////////// 1017 | 1018 | $pos = 0; 1019 | $res = array (); 1020 | $rlen = strlen($response); 1021 | for ( $i=0; $i $rlen ) 1027 | { 1028 | $this->_error = "incomplete reply"; 1029 | $this->_MBPop (); 1030 | return false; 1031 | } 1032 | $res[] = $len ? substr ( $response, $pos, $len ) : ""; 1033 | $pos += $len; 1034 | } 1035 | 1036 | $this->_MBPop (); 1037 | return $res; 1038 | } 1039 | 1040 | 1041 | ///////////////////////////////////////////////////////////////////////////// 1042 | // keyword generation 1043 | ///////////////////////////////////////////////////////////////////////////// 1044 | 1045 | /// connect to searchd server, and generate keyword list for a given query 1046 | /// returns false on failure, 1047 | /// an array of words on success 1048 | function BuildKeywords ( $query, $index, $hits ) 1049 | { 1050 | assert ( is_string($query) ); 1051 | assert ( is_string($index) ); 1052 | assert ( is_bool($hits) ); 1053 | 1054 | $this->_MBPush (); 1055 | 1056 | if (!( $fp = $this->_Connect() )) 1057 | { 1058 | $this->_MBPop(); 1059 | return false; 1060 | } 1061 | 1062 | ///////////////// 1063 | // build request 1064 | ///////////////// 1065 | 1066 | // v.1.0 req 1067 | $req = pack ( "N", strlen($query) ) . $query; // req query 1068 | $req .= pack ( "N", strlen($index) ) . $index; // req index 1069 | $req .= pack ( "N", (int)$hits ); 1070 | 1071 | //////////////////////////// 1072 | // send query, get response 1073 | //////////////////////////// 1074 | 1075 | $len = strlen($req); 1076 | $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header 1077 | $wrote = fwrite ( $fp, $req, $len+8 ); 1078 | if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) )) 1079 | { 1080 | $this->_MBPop (); 1081 | return false; 1082 | } 1083 | 1084 | ////////////////// 1085 | // parse response 1086 | ////////////////// 1087 | 1088 | $pos = 0; 1089 | $res = array (); 1090 | $rlen = strlen($response); 1091 | list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) ); 1092 | $pos += 4; 1093 | for ( $i=0; $i<$nwords; $i++ ) 1094 | { 1095 | list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; 1096 | $tokenized = $len ? substr ( $response, $pos, $len ) : ""; 1097 | $pos += $len; 1098 | 1099 | list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; 1100 | $normalized = $len ? substr ( $response, $pos, $len ) : ""; 1101 | $pos += $len; 1102 | 1103 | $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized ); 1104 | 1105 | if ( $hits ) 1106 | { 1107 | list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) ); 1108 | $pos += 8; 1109 | $res [$i]["docs"] = $ndocs; 1110 | $res [$i]["hits"] = $nhits; 1111 | } 1112 | 1113 | if ( $pos > $rlen ) 1114 | { 1115 | $this->_error = "incomplete reply"; 1116 | $this->_MBPop (); 1117 | return false; 1118 | } 1119 | } 1120 | 1121 | $this->_MBPop (); 1122 | return $res; 1123 | } 1124 | 1125 | function EscapeString ( $string ) 1126 | { 1127 | $from = array ( '(',')','|','-','!','@','~','"','&', '/' ); 1128 | $to = array ( '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/' ); 1129 | 1130 | return str_replace ( $from, $to, $string ); 1131 | } 1132 | 1133 | ///////////////////////////////////////////////////////////////////////////// 1134 | // attribute updates 1135 | ///////////////////////////////////////////////////////////////////////////// 1136 | 1137 | /// update given attribute values on given documents in given indexes 1138 | /// returns amount of updated documents (0 or more) on success, or -1 on failure 1139 | function UpdateAttributes ( $index, $attrs, $values ) 1140 | { 1141 | // verify everything 1142 | assert ( is_string($index) ); 1143 | 1144 | assert ( is_array($attrs) ); 1145 | foreach ( $attrs as $attr ) 1146 | assert ( is_string($attr) ); 1147 | 1148 | assert ( is_array($values) ); 1149 | foreach ( $values as $id=>$entry ) 1150 | { 1151 | assert ( is_numeric($id) ); 1152 | assert ( is_array($entry) ); 1153 | assert ( count($entry)==count($attrs) ); 1154 | foreach ( $entry as $v ) 1155 | assert ( is_int($v) ); 1156 | } 1157 | 1158 | // build request 1159 | $req = pack ( "N", strlen($index) ) . $index; 1160 | 1161 | $req .= pack ( "N", count($attrs) ); 1162 | foreach ( $attrs as $attr ) 1163 | $req .= pack ( "N", strlen($attr) ) . $attr; 1164 | 1165 | $req .= pack ( "N", count($values) ); 1166 | foreach ( $values as $id=>$entry ) 1167 | { 1168 | $req .= sphPack64 ( $id ); 1169 | foreach ( $entry as $v ) 1170 | $req .= pack ( "N", $v ); 1171 | } 1172 | 1173 | // mbstring workaround 1174 | $this->_MBPush (); 1175 | 1176 | // connect, send query, get response 1177 | if (!( $fp = $this->_Connect() )) 1178 | { 1179 | $this->_MBPop (); 1180 | return -1; 1181 | } 1182 | 1183 | $len = strlen($req); 1184 | $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header 1185 | fwrite ( $fp, $req, $len+8 ); 1186 | 1187 | if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) )) 1188 | { 1189 | $this->_MBPop (); 1190 | return -1; 1191 | } 1192 | 1193 | // parse response 1194 | list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) ); 1195 | $this->_MBPop (); 1196 | return $updated; 1197 | } 1198 | } 1199 | 1200 | // 1201 | // $Id$ 1202 | // -------------------------------------------------------------------------------- /sphinx_plugin/includes/search/fulltext_sphinx.php: -------------------------------------------------------------------------------- 1 | id = $config['avatar_salt']; 61 | $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; 62 | 63 | $this->sphinx = new SphinxClient (); 64 | 65 | if (!empty($config['fulltext_sphinx_configured'])) 66 | { 67 | if ($config['fulltext_sphinx_autorun'] && !file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid') && $this->index_created(true)) 68 | { 69 | $this->shutdown_searchd(); 70 | // $cwd = getcwd(); 71 | // chdir($config['fulltext_sphinx_bin_path']); 72 | exec($config['fulltext_sphinx_bin_path'] . SEARCHD_NAME . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf >> ' . $config['fulltext_sphinx_data_path'] . 'log/searchd-startup.log 2>&1 &'); 73 | // chdir($cwd); 74 | } 75 | 76 | // we only support localhost for now 77 | $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); 78 | } 79 | 80 | $config['fulltext_sphinx_min_word_len'] = 2; 81 | $config['fulltext_sphinx_max_word_len'] = 400; 82 | 83 | $error = false; 84 | } 85 | 86 | /** 87 | * Checks permissions and paths, if everything is correct it generates the config file 88 | */ 89 | function init() 90 | { 91 | global $db, $user, $config; 92 | 93 | if ($db->sql_layer != 'mysql' && $db->sql_layer != 'mysql4' && $db->sql_layer != 'mysqli') 94 | { 95 | return $user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; 96 | } 97 | 98 | if ($error = $this->config_updated()) 99 | { 100 | return $error; 101 | } 102 | 103 | // move delta to main index each hour 104 | set_config('search_gc', 3600); 105 | 106 | return false; 107 | } 108 | 109 | function config_updated() 110 | { 111 | global $db, $user, $config, $phpbb_root_path, $phpEx; 112 | 113 | if ($config['fulltext_sphinx_autoconf']) 114 | { 115 | $paths = array('fulltext_sphinx_bin_path', 'fulltext_sphinx_config_path', 'fulltext_sphinx_data_path'); 116 | 117 | // check for completeness and add trailing slash if it's not present 118 | foreach ($paths as $path) 119 | { 120 | if (empty($config[$path])) 121 | { 122 | return $user->lang['FULLTEXT_SPHINX_UNCONFIGURED']; 123 | } 124 | if ($config[$path] && substr($config[$path], -1) != '/') 125 | { 126 | set_config($path, $config[$path] . '/'); 127 | } 128 | } 129 | } 130 | 131 | $executables = array( 132 | $config['fulltext_sphinx_bin_path'] . INDEXER_NAME, 133 | $config['fulltext_sphinx_bin_path'] . SEARCHD_NAME, 134 | ); 135 | 136 | if ($config['fulltext_sphinx_autorun']) 137 | { 138 | foreach ($executables as $executable) 139 | { 140 | if (!file_exists($executable)) 141 | { 142 | return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_FOUND'], $executable); 143 | } 144 | 145 | if (!function_exists('exec')) 146 | { 147 | return $user->lang['FULLTEXT_SPHINX_REQUIRES_EXEC']; 148 | } 149 | 150 | $output = array(); 151 | @exec($executable, $output); 152 | 153 | $output = implode("\n", $output); 154 | if (strpos($output, 'Sphinx ') === false) 155 | { 156 | return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE'], $executable); 157 | } 158 | } 159 | } 160 | 161 | $writable_paths = array( 162 | $config['fulltext_sphinx_config_path'] => array('config' => 'fulltext_sphinx_autoconf', 'subdir' => false), 163 | $config['fulltext_sphinx_data_path'] => array('config' => 'fulltext_sphinx_autorun', 'subdir' => 'log'), 164 | $config['fulltext_sphinx_data_path'] . 'log/' => array('config' => 'fulltext_sphinx_autorun', 'subdir' => false), 165 | ); 166 | 167 | foreach ($writable_paths as $path => $info) 168 | { 169 | if ($config[$info['config']]) 170 | { 171 | // make sure directory exists 172 | // if we could drop the @ here and figure out whether the file really 173 | // doesn't exist or whether open_basedir is in effect, would be nice 174 | if (!@file_exists($path)) 175 | { 176 | return sprintf($user->lang['FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND'], $path); 177 | } 178 | 179 | // now check if it is writable by storing a simple file 180 | $filename = $path . 'write_test'; 181 | $fp = @fopen($filename, 'wb'); 182 | if ($fp === false) 183 | { 184 | return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); 185 | } 186 | @fclose($fp); 187 | 188 | @unlink($filename); 189 | 190 | if ($info['subdir'] !== false) 191 | { 192 | if (!is_dir($path . $info['subdir'])) 193 | { 194 | mkdir($path . $info['subdir']); 195 | } 196 | } 197 | } 198 | } 199 | 200 | if ($config['fulltext_sphinx_autoconf']) 201 | { 202 | include ($phpbb_root_path . 'config.' . $phpEx); 203 | 204 | // now that we're sure everything was entered correctly, generate a config for the index 205 | // we misuse the avatar_salt for this, as it should be unique ;-) 206 | 207 | if (!class_exists('sphinx_config')) 208 | { 209 | include($phpbb_root_path . 'includes/functions_sphinx.php'); 210 | } 211 | 212 | if (!file_exists($config['fulltext_sphinx_config_path'] . 'sphinx.conf')) 213 | { 214 | $filename = $config['fulltext_sphinx_config_path'] . 'sphinx.conf'; 215 | $fp = @fopen($filename, 'wb'); 216 | if ($fp === false) 217 | { 218 | return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); 219 | } 220 | @fclose($fp); 221 | } 222 | 223 | $config_object = new sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); 224 | 225 | $config_data = array( 226 | "source source_phpbb_{$this->id}_main" => array( 227 | array('type', 'mysql'), 228 | array('sql_host', $dbhost), 229 | array('sql_user', $dbuser), 230 | array('sql_pass', $dbpasswd), 231 | array('sql_db', $dbname), 232 | array('sql_port', $dbport), 233 | array('sql_query_pre', 'SET NAMES utf8'), 234 | array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), 235 | array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), 236 | array('sql_range_step', '5000'), 237 | array('sql_query', 'SELECT 238 | p.post_id AS id, 239 | p.forum_id, 240 | p.topic_id, 241 | p.poster_id, 242 | IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, 243 | p.post_time, 244 | p.post_subject, 245 | p.post_subject as title, 246 | p.post_text as data, 247 | t.topic_last_post_time, 248 | 0 as deleted 249 | FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t 250 | WHERE 251 | p.topic_id = t.topic_id 252 | AND p.post_id >= $start AND p.post_id <= $end'), 253 | array('sql_query_post', ''), 254 | array('sql_query_post_index', 'REPLACE INTO ' . SPHINX_TABLE . ' ( counter_id, max_doc_id ) VALUES ( 1, $maxid )'), 255 | array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'), 256 | array('sql_attr_uint', 'forum_id'), 257 | array('sql_attr_uint', 'topic_id'), 258 | array('sql_attr_uint', 'poster_id'), 259 | array('sql_attr_bool', 'topic_first_post'), 260 | array('sql_attr_bool', 'deleted'), 261 | array('sql_attr_timestamp' , 'post_time'), 262 | array('sql_attr_timestamp' , 'topic_last_post_time'), 263 | array('sql_attr_str2ordinal', 'post_subject'), 264 | ), 265 | "source source_phpbb_{$this->id}_delta : source_phpbb_{$this->id}_main" => array( 266 | array('sql_query_pre', ''), 267 | array('sql_query_range', ''), 268 | array('sql_range_step', ''), 269 | array('sql_query', 'SELECT 270 | p.post_id AS id, 271 | p.forum_id, 272 | p.topic_id, 273 | p.poster_id, 274 | IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, 275 | p.post_time, 276 | p.post_subject, 277 | p.post_subject as title, 278 | p.post_text as data, 279 | t.topic_last_post_time, 280 | 0 as deleted 281 | FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t 282 | WHERE 283 | p.topic_id = t.topic_id 284 | AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), 285 | ), 286 | "index index_phpbb_{$this->id}_main" => array( 287 | array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_main"), 288 | array('source', "source_phpbb_{$this->id}_main"), 289 | array('docinfo', 'extern'), 290 | array('morphology', 'none'), 291 | array('stopwords', (file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt') && $config['fulltext_sphinx_stopwords']) ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), 292 | array('min_word_len', '2'), 293 | array('charset_type', 'utf-8'), 294 | array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+ß410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), 295 | array('min_prefix_len', '0'), 296 | array('min_infix_len', '0'), 297 | ), 298 | "index index_phpbb_{$this->id}_delta : index_phpbb_{$this->id}_main" => array( 299 | array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_delta"), 300 | array('source', "source_phpbb_{$this->id}_delta"), 301 | ), 302 | 'indexer' => array( 303 | array('mem_limit', $config['fulltext_sphinx_indexer_mem_limit'] . 'M'), 304 | ), 305 | 'searchd' => array( 306 | array('address' , '127.0.0.1'), 307 | array('port', ($config['fulltext_sphinx_port']) ? $config['fulltext_sphinx_port'] : '3312'), 308 | array('log', $config['fulltext_sphinx_data_path'] . "log/searchd.log"), 309 | array('query_log', $config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), 310 | array('read_timeout', '5'), 311 | array('max_children', '30'), 312 | array('pid_file', $config['fulltext_sphinx_data_path'] . "searchd.pid"), 313 | array('max_matches', (string) MAX_MATCHES), 314 | ), 315 | ); 316 | 317 | $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true); 318 | $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true); 319 | 320 | foreach ($config_data as $section_name => $section_data) 321 | { 322 | $section = &$config_object->get_section_by_name($section_name); 323 | if (!$section) 324 | { 325 | $section = &$config_object->add_section($section_name); 326 | } 327 | 328 | foreach ($delete as $key => $void) 329 | { 330 | $section->delete_variables_by_name($key); 331 | } 332 | 333 | foreach ($non_unique as $key => $void) 334 | { 335 | $section->delete_variables_by_name($key); 336 | } 337 | 338 | foreach ($section_data as $entry) 339 | { 340 | $key = $entry[0]; 341 | $value = $entry[1]; 342 | 343 | if (!isset($non_unique[$key])) 344 | { 345 | $variable = &$section->get_variable_by_name($key); 346 | if (!$variable) 347 | { 348 | $variable = &$section->create_variable($key, $value); 349 | } 350 | else 351 | { 352 | $variable->set_value($value); 353 | } 354 | } 355 | else 356 | { 357 | $variable = &$section->create_variable($key, $value); 358 | } 359 | } 360 | } 361 | 362 | $config_object->write($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); 363 | } 364 | 365 | set_config('fulltext_sphinx_configured', '1'); 366 | 367 | $this->shutdown_searchd(); 368 | $this->tidy(); 369 | 370 | return false; 371 | } 372 | 373 | /** 374 | * Splits keywords entered by a user into an array of words stored in $this->split_words 375 | * Stores the tidied search query in $this->search_query 376 | * 377 | * @param string $keywords Contains the keyword as entered by the user 378 | * @param string $terms is either 'all' or 'any' 379 | * @return false if no valid keywords were found and otherwise true 380 | */ 381 | function split_keywords(&$keywords, $terms) 382 | { 383 | global $config; 384 | 385 | if ($terms == 'all') 386 | { 387 | $match = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#'); 388 | $replace = array(' & ', ' | ', ' - ', ' +', ' -', ' |', ''); 389 | 390 | $replacements = 0; 391 | $keywords = preg_replace($match, $replace, $keywords); 392 | $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED); 393 | } 394 | else 395 | { 396 | $this->sphinx->SetMatchMode(SPH_MATCH_ANY); 397 | } 398 | 399 | $match = array(); 400 | // Keep quotes 401 | $match[] = "#"#"; 402 | // KeepNew lines 403 | $match[] = "#[\n]+#"; 404 | 405 | $replace = array('"', " "); 406 | 407 | $keywords = str_replace(array('"', "\n"), array('"', ' '), trim($keywords)); 408 | 409 | if (strlen($keywords) > 0) 410 | { 411 | $this->search_query = str_replace('"', '"', $keywords); 412 | return true; 413 | } 414 | 415 | return false; 416 | } 417 | 418 | /** 419 | * Performs a search on keywords depending on display specific params. You have to run split_keywords() first. 420 | * 421 | * @param string $type contains either posts or topics depending on what should be searched for 422 | * @param string $fields contains either titleonly (topic titles should be searched), msgonly (only message bodies should be searched), firstpost (only subject and body of the first post should be searched) or all (all post bodies and subjects should be searched) 423 | * @param string $terms is either 'all' (use query as entered, words without prefix should default to "have to be in field") or 'any' (ignore search query parts and just return all posts that contain any of the specified words) 424 | * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query 425 | * @param string $sort_key is the key of $sort_by_sql for the selected sorting 426 | * @param string $sort_dir is either a or d representing ASC and DESC 427 | * @param string $sort_days specifies the maximum amount of days a post may be old 428 | * @param array $ex_fid_ary specifies an array of forum ids which should not be searched 429 | * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts 430 | * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched 431 | * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty 432 | * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match 433 | * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered 434 | * @param int $start indicates the first index of the page 435 | * @param int $per_page number of ids each page is supposed to contain 436 | * @return boolean|int total number of results 437 | * 438 | * @access public 439 | */ 440 | function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page) 441 | { 442 | global $config, $db, $auth; 443 | 444 | // No keywords? No posts. 445 | if (!strlen($this->search_query) && !sizeof($author_ary)) 446 | { 447 | return false; 448 | } 449 | 450 | $id_ary = array(); 451 | 452 | $join_topic = ($type == 'posts') ? false : true; 453 | 454 | // sorting 455 | 456 | if ($type == 'topics') 457 | { 458 | switch ($sort_key) 459 | { 460 | case 'a': 461 | $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); 462 | break; 463 | case 'f': 464 | $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); 465 | break; 466 | case 'i': 467 | case 's': 468 | $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); 469 | break; 470 | case 't': 471 | default: 472 | $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); 473 | break; 474 | } 475 | } 476 | else 477 | { 478 | switch ($sort_key) 479 | { 480 | case 'a': 481 | $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id'); 482 | break; 483 | case 'f': 484 | $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id'); 485 | break; 486 | case 'i': 487 | case 's': 488 | $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject'); 489 | break; 490 | case 't': 491 | default: 492 | $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time'); 493 | break; 494 | } 495 | } 496 | 497 | // most narrow filters first 498 | if ($topic_id) 499 | { 500 | $this->sphinx->SetFilter('topic_id', array($topic_id)); 501 | } 502 | 503 | $search_query_prefix = ''; 504 | 505 | switch($fields) 506 | { 507 | case 'titleonly': 508 | // only search the title 509 | if ($terms == 'all') 510 | { 511 | $search_query_prefix = '@title '; 512 | } 513 | $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // weight for the title 514 | $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post 515 | break; 516 | 517 | case 'msgonly': 518 | // only search the body 519 | if ($terms == 'all') 520 | { 521 | $search_query_prefix = '@data '; 522 | } 523 | $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5)); // weight for the body 524 | break; 525 | 526 | case 'firstpost': 527 | $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body 528 | $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post 529 | break; 530 | 531 | default: 532 | $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body 533 | break; 534 | } 535 | 536 | if (sizeof($author_ary)) 537 | { 538 | $this->sphinx->SetFilter('poster_id', $author_ary); 539 | } 540 | 541 | if (sizeof($ex_fid_ary)) 542 | { 543 | // All forums that a user is allowed to access 544 | $fid_ary = array_unique(array_intersect(array_keys($auth->acl_getf('f_read', true)), array_keys($auth->acl_getf('f_search', true)))); 545 | // All forums that the user wants to and can search in 546 | $search_forums = array_diff($fid_ary, $ex_fid_ary); 547 | 548 | if (sizeof($search_forums)) 549 | { 550 | $this->sphinx->SetFilter('forum_id', $search_forums); 551 | } 552 | } 553 | 554 | $this->sphinx->SetFilter('deleted', array(0)); 555 | 556 | $this->sphinx->SetLimits($start, (int) $per_page, MAX_MATCHES); 557 | $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); 558 | 559 | // could be connection to localhost:3312 failed (errno=111, msg=Connection refused) during rotate, retry if so 560 | $retries = CONNECT_RETRIES; 561 | while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) 562 | { 563 | usleep(CONNECT_WAIT_TIME); 564 | $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); 565 | } 566 | 567 | $id_ary = array(); 568 | if (isset($result['matches'])) 569 | { 570 | if ($type == 'posts') 571 | { 572 | $id_ary = array_keys($result['matches']); 573 | } 574 | else 575 | { 576 | foreach($result['matches'] as $key => $value) 577 | { 578 | $id_ary[] = $value['attrs']['topic_id']; 579 | } 580 | } 581 | } 582 | else 583 | { 584 | return false; 585 | } 586 | 587 | $result_count = $result['total_found']; 588 | 589 | $id_ary = array_slice($id_ary, 0, (int) $per_page); 590 | 591 | return $result_count; 592 | } 593 | 594 | /** 595 | * Performs a search on an author's posts without caring about message contents. Depends on display specific params 596 | * 597 | * @param string $type contains either posts or topics depending on what should be searched for 598 | * @param boolean $firstpost_only if true, only topic starting posts will be considered 599 | * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query 600 | * @param string $sort_key is the key of $sort_by_sql for the selected sorting 601 | * @param string $sort_dir is either a or d representing ASC and DESC 602 | * @param string $sort_days specifies the maximum amount of days a post may be old 603 | * @param array $ex_fid_ary specifies an array of forum ids which should not be searched 604 | * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts 605 | * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched 606 | * @param array $author_ary an array of author ids 607 | * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match 608 | * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered 609 | * @param int $start indicates the first index of the page 610 | * @param int $per_page number of ids each page is supposed to contain 611 | * @return boolean|int total number of results 612 | * 613 | * @access public 614 | */ 615 | function author_search($type, $firstpost_only, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page) 616 | { 617 | $this->search_query = ''; 618 | 619 | $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN); 620 | $fields = ($firstpost_only) ? 'firstpost' : 'all'; 621 | $terms = 'all'; 622 | return $this->keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, $id_ary, $start, $per_page); 623 | } 624 | 625 | /** 626 | * Updates wordlist and wordmatch tables when a message is posted or changed 627 | * 628 | * @param string $mode Contains the post mode: edit, post, reply, quote 629 | * @param int $post_id The id of the post which is modified/created 630 | * @param string &$message New or updated post content 631 | * @param string &$subject New or updated post subject 632 | * @param int $poster_id Post author's user id 633 | * @param int $forum_id The id of the forum in which the post is located 634 | * 635 | * @access public 636 | */ 637 | function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id) 638 | { 639 | global $config, $db; 640 | 641 | if ($mode == 'edit') 642 | { 643 | $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int)$post_id => array((int)$forum_id, (int)$poster_id))); 644 | } 645 | else if ($mode != 'post' && $post_id) 646 | { 647 | // update topic_last_post_time for full topic 648 | $sql = 'SELECT p1.post_id 649 | FROM ' . POSTS_TABLE . ' p1 650 | LEFT JOIN ' . POSTS_TABLE . ' p2 ON (p1.topic_id = p2.topic_id) 651 | WHERE p2.post_id = ' . $post_id; 652 | $result = $db->sql_query($sql); 653 | 654 | $post_updates = array(); 655 | $post_time = time(); 656 | while ($row = $db->sql_fetchrow($result)) 657 | { 658 | $post_updates[(int)$row['post_id']] = array((int) $post_time); 659 | } 660 | $db->sql_freeresult($result); 661 | 662 | if (sizeof($post_updates)) 663 | { 664 | $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates); 665 | } 666 | } 667 | 668 | if ($config['fulltext_sphinx_autorun']) 669 | { 670 | if ($this->index_created()) 671 | { 672 | $rotate = ($this->searchd_running()) ? ' --rotate' : ''; 673 | 674 | $cwd = getcwd(); 675 | chdir($config['fulltext_sphinx_bin_path']); 676 | exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); 677 | var_dump('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); 678 | chdir($cwd); 679 | } 680 | } 681 | } 682 | 683 | /** 684 | * Delete a post from the index after it was deleted 685 | */ 686 | function index_remove($post_ids, $author_ids, $forum_ids) 687 | { 688 | $values = array(); 689 | foreach ($post_ids as $post_id) 690 | { 691 | $values[$post_id] = array(1); 692 | } 693 | 694 | $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values); 695 | } 696 | 697 | /** 698 | * Destroy old cache entries 699 | */ 700 | function tidy($create = false) 701 | { 702 | global $config; 703 | 704 | if ($config['fulltext_sphinx_autorun']) 705 | { 706 | if ($this->index_created() || $create) 707 | { 708 | $rotate = ($this->searchd_running()) ? ' --rotate' : ''; 709 | 710 | $cwd = getcwd(); 711 | chdir($config['fulltext_sphinx_bin_path']); 712 | exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_main >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); 713 | exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); 714 | chdir($cwd); 715 | } 716 | } 717 | 718 | set_config('search_last_gc', time(), true); 719 | } 720 | 721 | /** 722 | * Create sphinx table 723 | */ 724 | function create_index($acp_module, $u_action) 725 | { 726 | global $db, $user, $config; 727 | 728 | $this->shutdown_searchd(); 729 | 730 | if (!isset($config['fulltext_sphinx_configured']) || !$config['fulltext_sphinx_configured']) 731 | { 732 | $user->add_lang('mods/fulltext_sphinx'); 733 | 734 | return $user->lang['FULLTEXT_SPHINX_CONFIGURE_FIRST']; 735 | } 736 | 737 | if (!$this->index_created()) 738 | { 739 | $sql = 'CREATE TABLE IF NOT EXISTS ' . SPHINX_TABLE . ' ( 740 | counter_id INT NOT NULL PRIMARY KEY, 741 | max_doc_id INT NOT NULL 742 | )'; 743 | $db->sql_query($sql); 744 | 745 | $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE; 746 | $db->sql_query($sql); 747 | } 748 | 749 | // start indexing process 750 | $this->tidy(true); 751 | 752 | $this->shutdown_searchd(); 753 | 754 | return false; 755 | } 756 | 757 | /** 758 | * Drop sphinx table 759 | */ 760 | function delete_index($acp_module, $u_action) 761 | { 762 | global $db, $config; 763 | 764 | $this->shutdown_searchd(); 765 | 766 | if ($config['fulltext_sphinx_autorun']) 767 | { 768 | sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^index_phpbb_' . $this->id . '.*$#'); 769 | } 770 | 771 | if (!$this->index_created()) 772 | { 773 | return false; 774 | } 775 | 776 | $sql = 'DROP TABLE ' . SPHINX_TABLE; 777 | $db->sql_query($sql); 778 | 779 | $this->shutdown_searchd(); 780 | 781 | return false; 782 | } 783 | 784 | /** 785 | * Returns true if the sphinx table was created 786 | */ 787 | function index_created($allow_new_files = true) 788 | { 789 | global $db, $config; 790 | 791 | $sql = 'SHOW TABLES LIKE \'' . SPHINX_TABLE . '\''; 792 | $result = $db->sql_query($sql); 793 | $row = $db->sql_fetchrow($result); 794 | $db->sql_freeresult($result); 795 | 796 | $created = false; 797 | 798 | if ($row) 799 | { 800 | if ($config['fulltext_sphinx_autorun']) 801 | { 802 | if ((file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.spd')) || ($allow_new_files && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.new.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.new.spd'))) 803 | { 804 | $created = true; 805 | } 806 | } 807 | else 808 | { 809 | $created = true; 810 | } 811 | } 812 | 813 | return $created; 814 | } 815 | 816 | /** 817 | * Kills the searchd process and makes sure there's no locks left over 818 | */ 819 | function shutdown_searchd() 820 | { 821 | global $config; 822 | 823 | if ($config['fulltext_sphinx_autorun']) 824 | { 825 | if (!function_exists('exec')) 826 | { 827 | set_config('fulltext_sphinx_autorun', '0'); 828 | return; 829 | } 830 | 831 | exec('killall -9 ' . SEARCHD_NAME . ' >> /dev/null 2>&1 &'); 832 | 833 | if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) 834 | { 835 | unlink($config['fulltext_sphinx_data_path'] . 'searchd.pid'); 836 | } 837 | 838 | sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^.*\.spl$#'); 839 | } 840 | } 841 | 842 | /** 843 | * Checks whether searchd is running, if it's not running it makes sure there's no left over 844 | * files by calling shutdown_searchd. 845 | * 846 | * @return boolean Whether searchd is running or not 847 | */ 848 | function searchd_running() 849 | { 850 | global $config; 851 | 852 | // if we cannot manipulate the service assume it is running 853 | if (!$config['fulltext_sphinx_autorun']) 854 | { 855 | return true; 856 | } 857 | 858 | if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) 859 | { 860 | $pid = trim(file_get_contents($config['fulltext_sphinx_data_path'] . 'searchd.pid')); 861 | 862 | if ($pid) 863 | { 864 | $output = array(); 865 | $pidof_command = 'pidof'; 866 | 867 | exec('whereis -b pidof', $output); 868 | if (sizeof($output) > 1) 869 | { 870 | $output = explode(' ', trim($output[0])); 871 | $pidof_command = $output[1]; // 0 is pidof: 872 | } 873 | 874 | $output = array(); 875 | exec($pidof_command . ' ' . SEARCHD_NAME, $output); 876 | if (sizeof($output) && (trim($output[0]) == $pid || trim($output[1]) == $pid)) 877 | { 878 | return true; 879 | } 880 | } 881 | } 882 | 883 | // make sure it's really not running 884 | $this->shutdown_searchd(); 885 | 886 | return false; 887 | } 888 | 889 | /** 890 | * Returns an associative array containing information about the indexes 891 | */ 892 | function index_stats() 893 | { 894 | global $user; 895 | 896 | if (empty($this->stats)) 897 | { 898 | $this->get_stats(); 899 | } 900 | 901 | $user->add_lang('mods/fulltext_sphinx'); 902 | 903 | return array( 904 | $user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, 905 | $user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, 906 | $user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0, 907 | $user->lang['FULLTEXT_SPHINX_LAST_SEARCHES'] => nl2br($this->stats['last_searches']), 908 | ); 909 | } 910 | 911 | /** 912 | * Collects stats that can be displayed on the index maintenance page 913 | */ 914 | function get_stats() 915 | { 916 | global $db, $config; 917 | 918 | if ($this->index_created()) 919 | { 920 | $sql = 'SELECT COUNT(post_id) as total_posts 921 | FROM ' . POSTS_TABLE; 922 | $result = $db->sql_query($sql); 923 | $this->stats['total_posts'] = (int) $db->sql_fetchfield('total_posts'); 924 | $db->sql_freeresult($result); 925 | 926 | $sql = 'SELECT COUNT(p.post_id) as main_posts 927 | FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m 928 | WHERE p.post_id <= m.max_doc_id 929 | AND m.counter_id = 1'; 930 | $result = $db->sql_query($sql); 931 | $this->stats['main_posts'] = (int) $db->sql_fetchfield('main_posts'); 932 | $db->sql_freeresult($result); 933 | } 934 | 935 | $this->stats['last_searches'] = ''; 936 | if ($config['fulltext_sphinx_autorun']) 937 | { 938 | if (file_exists($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log')) 939 | { 940 | $last_searches = explode("\n", utf8_htmlspecialchars(sphinx_read_last_lines($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log', 3))); 941 | 942 | foreach($last_searches as $i => $search) 943 | { 944 | if (strpos($search, '[' . $this->indexes . ']') !== false) 945 | { 946 | $last_searches[$i] = str_replace('[' . $this->indexes . ']', '', $search); 947 | } 948 | else 949 | { 950 | $last_searches[$i] = ''; 951 | } 952 | } 953 | $this->stats['last_searches'] = implode("\n", $last_searches); 954 | } 955 | } 956 | } 957 | 958 | /** 959 | * Returns a list of options for the ACP to display 960 | */ 961 | function acp() 962 | { 963 | global $user, $config; 964 | 965 | $user->add_lang('mods/fulltext_sphinx'); 966 | 967 | $config_vars = array( 968 | 'fulltext_sphinx_autoconf' => 'bool', 969 | 'fulltext_sphinx_autorun' => 'bool', 970 | 'fulltext_sphinx_config_path' => 'string', 971 | 'fulltext_sphinx_data_path' => 'string', 972 | 'fulltext_sphinx_bin_path' => 'string', 973 | 'fulltext_sphinx_port' => 'int', 974 | 'fulltext_sphinx_stopwords' => 'bool', 975 | 'fulltext_sphinx_indexer_mem_limit' => 'int', 976 | ); 977 | 978 | $defaults = array( 979 | 'fulltext_sphinx_autoconf' => '1', 980 | 'fulltext_sphinx_autorun' => '1', 981 | 'fulltext_sphinx_indexer_mem_limit' => '512', 982 | ); 983 | 984 | foreach ($config_vars as $config_var => $type) 985 | { 986 | if (!isset($config[$config_var])) 987 | { 988 | $default = ''; 989 | if (isset($defaults[$config_var])) 990 | { 991 | $default = $defaults[$config_var]; 992 | } 993 | set_config($config_var, $default); 994 | } 995 | } 996 | 997 | $no_autoconf = false; 998 | $no_autorun = false; 999 | $bin_path = $config['fulltext_sphinx_bin_path']; 1000 | 1001 | // try to guess the path if it is empty 1002 | if (empty($bin_path)) 1003 | { 1004 | if (@file_exists('/usr/local/bin/' . INDEXER_NAME) && @file_exists('/usr/local/bin/' . SEARCHD_NAME)) 1005 | { 1006 | $bin_path = '/usr/local/bin/'; 1007 | } 1008 | else if (@file_exists('/usr/bin/' . INDEXER_NAME) && @file_exists('/usr/bin/' . SEARCHD_NAME)) 1009 | { 1010 | $bin_path = '/usr/bin/'; 1011 | } 1012 | else 1013 | { 1014 | $output = array(); 1015 | if (!function_exists('exec') || null === @exec('whereis -b ' . INDEXER_NAME, $output)) 1016 | { 1017 | $no_autorun = true; 1018 | } 1019 | else if (sizeof($output)) 1020 | { 1021 | $output = explode(' ', $output[0]); 1022 | array_shift($output); // remove indexer: 1023 | 1024 | foreach ($output as $path) 1025 | { 1026 | $path = dirname($path) . '/'; 1027 | 1028 | if (file_exists($path . INDEXER_NAME) && file_exists($path . SEARCHD_NAME)) 1029 | { 1030 | $bin_path = $path; 1031 | break; 1032 | } 1033 | } 1034 | } 1035 | } 1036 | } 1037 | 1038 | if ($no_autorun) 1039 | { 1040 | set_config('fulltext_sphinx_autorun', '0'); 1041 | } 1042 | 1043 | if ($no_autoconf) 1044 | { 1045 | set_config('fulltext_sphinx_autoconf', '0'); 1046 | } 1047 | 1048 | // rewrite config if fulltext sphinx is enabled 1049 | if ($config['fulltext_sphinx_autoconf'] && isset($config['fulltext_sphinx_configured']) && $config['fulltext_sphinx_configured']) 1050 | { 1051 | $this->config_updated(); 1052 | } 1053 | 1054 | // check whether stopwords file is available and enabled 1055 | if (@file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt')) 1056 | { 1057 | $stopwords_available = true; 1058 | $stopwords_active = $config['fulltext_sphinx_stopwords']; 1059 | } 1060 | else 1061 | { 1062 | $stopwords_available = false; 1063 | $stopwords_active = false; 1064 | set_config('fulltext_sphinx_stopwords', '0'); 1065 | } 1066 | 1067 | $tpl = ' 1068 | ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. ' 1069 |
1070 |

' . $user->lang['FULLTEXT_SPHINX_AUTOCONF_EXPLAIN'] . '
1071 |
1072 |
1073 |
1074 |

' . $user->lang['FULLTEXT_SPHINX_AUTORUN_EXPLAIN'] . '
1075 |
1076 |
1077 |
1078 |

' . $user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
1079 |
1080 |
1081 |
1082 |

' . $user->lang['FULLTEXT_SPHINX_BIN_PATH_EXPLAIN'] . '
1083 |
1084 |
1085 |
1086 |

' . $user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
1087 |
1088 |
1089 | ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. ' 1090 |
1091 |

' . $user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
1092 |
1093 |
1094 |
1095 |

' . $user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
1096 |
1097 |
1098 |
1099 |

' . $user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '
1100 |
' . $user->lang['MIB'] . '
1101 |
1102 | '; 1103 | 1104 | // These are fields required in the config table 1105 | return array( 1106 | 'tpl' => $tpl, 1107 | 'config' => $config_vars 1108 | ); 1109 | } 1110 | } 1111 | 1112 | /** 1113 | * Deletes all files from a directory that match a certain pattern 1114 | * 1115 | * @param string $path Path from which files shall be deleted 1116 | * @param string $pattern PCRE pattern that a file needs to match in order to be deleted 1117 | */ 1118 | function sphinx_unlink_by_pattern($path, $pattern) 1119 | { 1120 | $dir = opendir($path); 1121 | while (false !== ($file = readdir($dir))) 1122 | { 1123 | if (is_file($path . $file) && preg_match($pattern, $file)) 1124 | { 1125 | unlink($path . $file); 1126 | } 1127 | } 1128 | closedir($dir); 1129 | } 1130 | 1131 | /** 1132 | * Reads the last from a file 1133 | * 1134 | * @param string $file The filename from which the lines shall be read 1135 | * @param int $amount The number of lines to be read from the end 1136 | * @return string Last lines of the file 1137 | */ 1138 | function sphinx_read_last_lines($file, $amount) 1139 | { 1140 | $fp = fopen($file, 'r'); 1141 | fseek($fp, 0, SEEK_END); 1142 | 1143 | $c = ''; 1144 | $i = 0; 1145 | 1146 | while ($i < $amount) 1147 | { 1148 | fseek($fp, -2, SEEK_CUR); 1149 | $c = fgetc($fp); 1150 | if ($c == "\n") 1151 | { 1152 | $i++; 1153 | } 1154 | if (feof($fp)) 1155 | { 1156 | break; 1157 | } 1158 | } 1159 | 1160 | $string = fread($fp, 8192); 1161 | fclose($fp); 1162 | 1163 | return $string; 1164 | } 1165 | 1166 | ?> -------------------------------------------------------------------------------- /sphinx_mod/modx.prosilver.en.xsl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | ]> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 475 | phpBB MOD » <xsl:value-of select="$title" /> 476 | 1417 | 1418 | 1419 |
1420 |
1421 | 1429 |
1430 |
1431 |
1432 |
1433 |
1434 | 1435 | 1436 | 1437 |
1438 |
1439 | 1440 | 1441 | 1442 |
1443 |
1444 |

Save all files. End of MOD.

1445 |

You have finished the installation for this MOD. Upload all changed files to your website. If the installation went bad, simply restore your backed up files.

1446 |
1447 |
1448 |
1449 |
1450 |
1451 |
1452 |
1453 | 1456 |
1457 | 1458 | 1459 |
1460 | 1461 |
1462 | About this MOD 1463 |
1464 | 1465 |
1466 |
Title:
1467 |
1468 | 1469 |
1470 | 1471 |
1472 |
1473 |

1474 |
1475 |
1476 |
1477 |
1478 | 1479 |

1480 |
1481 |
1482 |
Description:
1483 |
1484 | 1485 |
1486 | 1487 |
1488 |
1489 |

1490 | 1491 | 1492 | 1493 |

1494 |
1495 |
1496 |
1497 |
1498 | 1499 |

1500 | 1501 | 1502 | 1503 |

1504 |
1505 |
1506 |
Version:
1507 |
1508 |

1509 | 1510 | 1511 | 1512 |

1513 |
1514 | 1515 | 1516 | 1517 | 1518 |
Author Notes:
1519 |
1520 | 1521 |
1522 | 1523 |
1524 |
1525 |

1526 | 1527 | 1528 | 1529 |

1530 |
1531 |
1532 |
1533 |
1534 | 1535 |

1536 | 1537 | 1538 | 1539 |

1540 |
1541 |
1542 |
1543 |
1544 | 1545 |
1546 |
1547 |
1548 | 1549 | 1550 | Authors 1551 | 1552 | 1553 | Author 1554 | 1555 | 1556 | 1557 |
1558 | 1559 |

Files to Edit

1560 | 1561 | 1562 | 1563 |
1564 |

Included Files

1565 | 1566 |

No files have been included with this MOD.

1567 |
1568 | 1569 | 1570 | 1571 |

Additional MODX Files

1572 | 1573 |

This MOD has no additional MODX files.

1574 |
1575 | 1580 |
1581 |
1582 |

Disclaimer & Other Notes

1583 |
1584 | 1585 |
1586 |

For security purposes, please check: http://www.phpbb.com/mods/ for the latest version of this MOD. Downloading this MOD from other sites could cause malicious code to enter into your phpBB Forum. As such, phpBB will not offer support for MODs not offered in our MODs database, located at: http://www.phpbb.com/mods/

1587 |

Before adding this MOD to your forum, you should back up all files related to this MOD.

1588 |

This MOD was designed for phpBB and may not function as stated on other phpBB versions. MODs for phpBB 3.0 will not work on phpBB 2.0 and vice versa.

1589 | 1590 | 1591 |

This MOD is development quality. It is not recommended that you install it on a live forum.

1592 |
1593 |
1594 |
1595 | 1596 |
1597 |
1598 |
1599 |

License & English Support

1600 |
1601 | 1602 |
1603 |

This MOD has been licensed under the following license:

1604 |

1605 |

English support can be obtained at http://www.phpbb.com/mods/ for released MODs.

1606 |
1607 | 1608 |
1609 | 1610 | 1611 | 1612 |
1613 |
1614 | 1615 | 1616 |
1617 | 1618 | 1619 | 1620 |
1621 |
1622 |
1623 | 1624 |
1625 |
Username:
1626 |
1627 | 1628 |
Email:
1629 |
1630 |
1631 | 1632 |
Name:
1633 |
1634 |
1635 | 1636 |
WWW:
1637 |
1638 |
1639 | 1640 |
Contributions:
1641 |
1642 | 1643 |
1644 | From:
1645 | To: 1646 |
1647 |
1648 |
1649 |
1650 |
1651 | 1652 |
Installation Level:
1653 |
1654 |
1655 | 1656 |

Easy

1657 |
1658 | 1659 |

Intermediate

1660 |
1661 | 1662 |

Advanced

1663 |
1664 |
1665 |
1666 |
Installation Time:
1667 |
1668 |
1669 |

~ minutes

1670 |
1671 |
1672 |
1673 | 1674 | 1675 |
1676 | MOD History 1677 | 1684 |
1685 | 1686 | 1687 | 1688 |
1689 |
1690 |
1691 |
1692 | 1693 |
1694 | 1695 |
1696 |
1697 |

 - Version 1698 | 1699 | 1700 | 1701 |

1702 |
1703 |

1704 | 1705 |
1706 | 1707 | 1708 | 1709 |
1710 |
1711 | 1712 | 1713 | 1714 | 1715 | 1716 |
1717 |
1718 | 1719 |
1720 |
1721 | 1722 |
1723 |
1724 |
    1725 | 1726 |
  • 1727 |

    1728 |
  • 1729 |
    1730 |
1731 |
1732 |
1733 | 1734 |
    1735 | 1736 |
  • 1737 |

    1738 |
  • 1739 |
    1740 |
1741 |
1742 | 1743 |
    1744 | 1745 | 1746 | 1747 |
1748 |
1749 | 1750 |
    1751 | 1752 | 1753 | 1754 |
1755 |
1756 | 1757 |
  • ,
  • 1758 |
    1759 | 1760 | 1761 |
  • 1762 | , 1763 | 1764 |
  • 1765 |
    1766 |
    1767 | 1768 | 1769 |
    1770 |
    1771 | 1772 | 1781 |
    1782 |
    1783 |

    SQL

    1784 |
    1785 | 1786 | 1787 | 1788 | 1789 |
    1790 |
    1791 | 1792 | 1793 | 1794 | 1795 | 1796 | 1797 |

    Edits

    1798 |

    s<>Use your keyboard to navigate the code boxes. You may also hit 's' on your keyboard to go to the first code box.

    1799 |
    1800 |
    1801 | 1802 | 1803 | 1804 |
    1805 |
    1806 |
    1807 | 1808 |
    1809 | 1810 | 1811 |
    1812 | 1813 | : 1814 | 1815 |
    1816 | 1817 |
    1818 |
    1819 |
    1820 |
    1821 |
    1822 | 1823 | 1824 |

    DIY Instructions

    1825 |
    1826 | 1827 |
    1828 |

    These are manual instructions that cannot be performed automatically. You should follow these instructions carefully.

    1829 |
    1830 |
    1831 | 1832 |
    1833 |
    1834 |
    1835 | 1836 |
    1837 |
    1838 |
    1839 |
    1840 |
    1841 |
    1842 | 1843 |
    1844 |
    1845 |
    1846 | 1847 |
    1848 | 1849 |
    1850 |

    Open: 

    1851 | 1852 |
    1853 |
    1854 | 1855 |
    1856 |

    Comments

    1857 |
    1858 | 1859 |
    1860 | 1861 | 1862 | 1863 |
    1864 |
    1865 |
    1866 |
    1867 |
    1868 |
    1869 | 1870 | 1871 |

    Find

    1872 |

    Tip: This may be a partial find and not the whole line. 1873 | 1874 |
    This find contains an advanced feature known as regular expressions, click here to learn more. 1875 |
    1876 |

    1877 |
    1878 | 1879 |
    1880 |
    1881 |
    1882 | 1883 | 1884 |

    Add after

    1885 |

    Tip: Add these lines on a new blank line after the preceding line(s) to find.

    1886 |
    1887 | 1888 |

    Add before

    1889 |

    Tip: Add these lines on a new blank line before the preceding line(s) to find.

    1890 |
    1891 | 1892 |

    Replace With

    1893 |

    Tip: Replace the preceding line(s) to find with the following lines.

    1894 |
    1895 | 1896 |

    Increment

    1897 |

    Tip: This allows you to alter integers. For help on what each operator means, click here.

    1898 |
    1899 |
    1900 | 1901 |
    1902 |
    1903 |
    1904 | 1905 |
    1906 | 1907 | 1908 |
    In-line Find
    1909 |

    Tip: This is a partial match of a line for in-line operations. 1910 | 1911 |
    This find contains an advanced feature known as regular expressions, click here to learn more. 1912 |
    1913 |

    1914 |
    1915 | 1916 |
    1917 |
    1918 |
    1919 | 1920 | 1921 |
    In-line Add after
    1922 |

    1923 |
    1924 | 1925 |
    In-line Add before
    1926 |

    1927 |
    1928 | 1929 |
    In-line Replace With
    1930 |

    1931 |
    1932 | 1933 |
    In-line Increment
    1934 |

    Tip: This allows you to alter integers. For help on what each operator means, click here.

    1935 |
    1936 |
    1937 | 1938 |
    1939 |
    1940 |
    1941 | 1942 |
    1943 |
    Comments 
    1944 |
    1945 |
    1946 |
    1947 |
    1948 |
    1949 |
    1950 |
    1951 |
    1952 | << Hide 1953 |
    1954 |
    1955 |
    1956 | 1957 |
    1958 |
    1959 | 1960 |

    File Copy

    1961 |
      1962 | 1963 |
    1. 1964 |
      Copy: 
      1965 |
      To: 
      1966 |
      1967 |
    2. 1968 |
      1969 |
    1970 |
    1971 | 1972 | 1973 | 1974 | 1975 | 1976 |
    1977 | 1978 |
    1979 | 1980 | 1981 | 1982 |
    1983 |
    1984 |
    --------------------------------------------------------------------------------