Elgg 検索の日本語対応

Elgg で検索に日本語を使用すると、稀に検索できてほぼ失敗、という結果になるので調査してみました。

まず、どういうSQL文が実行されているか確認してみます。「検索語」を検索してみたときの SQL の一部です。

SELECT count(DISTINCT e.guid) as total 
FROM entities e  JOIN objects_entity oe ON e.guid = oe.guid  
WHERE  (MATCH (oe.title,oe.description) AGAINST ('+検索語' IN BOOLEAN MODE))
  AND  ((e.type = 'object' AND e.subtype IN (4)))
  AND  (e.site_guid IN (1))
  AND ( (e.access_id = -2
			AND e.owner_guid IN (
				SELECT guid_one FROM entity_relationships
				WHERE relationship='friend' AND guid_two=41
			)) OR  (e.access_id IN (2,1,3)
			OR (e.owner_guid = 41)
			OR (
				e.access_id = 0
				AND e.owner_guid = 41
			)
		) and e.enabled='yes')
全文検索が行われているため、偶々空白で区切られていたものだけ検索でヒットしていたようです。

このSQL分を組み立てている箇所を探します。検索機能もモジュール化されていて、/mod/search/ にソースがあります。このディレクトリ内を探してみると /mod/search/start.php にあります。
/**
 * Returns a where clause for a search query.
 *
 * @param str $table Prefix for table to search on
 * @param array $fields Fields to match against
 * @param array $params Original search params
 * @return str
 */
function search_get_where_sql($table, $fields, $params, $use_fulltext = TRUE) {
	global $CONFIG;
	$query = $params['query'];

	// add the table prefix to the fields
	foreach ($fields as $i => $field) {
		if ($table) {
			$fields[$i] = "$table.$field";
		}
	}
	
	$where = '';

	// if query is shorter than the min for fts words
	// it's likely a single acronym or similar
	// switch to literal mode
	if (elgg_strlen($query) < $CONFIG->search_info['min_chars']) {
		$likes = array();
		$query = sanitise_string($query);
		foreach ($fields as $field) {
			$likes[] = "$field LIKE '%$query%'";
		}
		$likes_str = implode(' OR ', $likes);
		$where = "($likes_str)";
	} else {
		// if we're not using full text, rewrite the query for bool mode.
		// exploiting a feature(ish) of bool mode where +-word is the same as -word
		if (!$use_fulltext) {
			$query = '+' . str_replace(' ', ' +', $query);
		}
		
		// if using advanced, boolean operators, or paired "s, switch into boolean mode
		$booleans_used = preg_match("/([\-\+~])([\w]+)/i", $query);
		$advanced_search = (isset($params['advanced_search']) && $params['advanced_search']);
		$quotes_used = (elgg_substr_count($query, '"') >= 2); 
		
		if (!$use_fulltext || $booleans_used || $advanced_search || $quotes_used) {
			$options = 'IN BOOLEAN MODE';
		} else {
			// natural language mode is default and this keyword isn't supported in < 5.1
			//$options = 'IN NATURAL LANGUAGE MODE';
			$options = '';
		}
		
		// if short query, use query expansion.
		// @todo doesn't seem to be working well.
//		if (elgg_strlen($query) < 5) {
//			$options .= ' WITH QUERY EXPANSION';
//		}
		$query = sanitise_string($query);

		$fields_str = implode(',', $fields);
		$where = "(MATCH ($fields_str) AGAINST ('$query' $options))";
	}

	return $where;
}

$use_fulltext というパラメータがありますが、BOOLEAN MODE で検索をするか否かのフラグで全文検索するか否かの指定ではありません。/mod/search 内では全て $use_fulltext = FALSE として search_get_where_sql() が呼び出されています。
409行目の if 文で、検索文字列が $CONFIG->search_info['min_chars'] より小さい場合は LIKE 検索をするようなので、この処理を強制的に有効にしてみます。
410 ~ 416 行目までを有効にして、その他の 409 ~ 446 行目までをコメントアウトしてしまっても良いですが、$CONFIG->search_info['min_chars'] を設定している箇所を調べてみます。
これも、start.php にあります。
/**
 * Initialize search plugin
 */
function search_init() {
	global $CONFIG;
	require_once 'search_hooks.php';

	// page handler for search actions and results
	elgg_register_page_handler('search', 'search_page_handler');

	// register some default search hooks
	elgg_register_plugin_hook_handler('search', 'object', 'search_objects_hook');
	elgg_register_plugin_hook_handler('search', 'user', 'search_users_hook');
	elgg_register_plugin_hook_handler('search', 'group', 'search_groups_hook');

	// tags and comments are a bit different.
	// register a search types and a hooks for them.
	elgg_register_plugin_hook_handler('search_types', 'get_types', 'search_custom_types_tags_hook');
	elgg_register_plugin_hook_handler('search', 'tags', 'search_tags_hook');

	elgg_register_plugin_hook_handler('search_types', 'get_types', 'search_custom_types_comments_hook');
	elgg_register_plugin_hook_handler('search', 'comments', 'search_comments_hook');

	// get server min and max allowed chars for ft searching
	$CONFIG->search_info = array();

	// can't use get_data() here because some servers don't have these globals set,
	// which throws a db exception.
	$dblink = get_db_link('read');
	$r = mysql_query('SELECT @@ft_min_word_len as min, @@ft_max_word_len as max', $dblink);
	if ($r && ($word_lens = mysql_fetch_assoc($r))) {
		$CONFIG->search_info['min_chars'] = $word_lens['min'];
		$CONFIG->search_info['max_chars'] = $word_lens['max'];
	} else {
		// uhhh these are good numbers.
		$CONFIG->search_info['min_chars'] = 4;
		$CONFIG->search_info['max_chars'] = 90;
	}

	// add in CSS for search elements
	elgg_extend_view('css/elgg', 'search/css');

	// extend view for elgg topbar search box
	elgg_extend_view('page/elements/header', 'search/header');
}
全文検索インデックスの最小文字数を $CONFIG->search_info['min_chars'] に設定していて、検索文字列がこれよりも短い場合は、LIKE 検索を行うという仕様になっている様です。
今回は全文検索にはしないので、ここの処理自体が不要ですので以下のように変更します。
	// can't use get_data() here because some servers don't have these globals set,
	// which throws a db exception.
/*
	$dblink = get_db_link('read');
	$r = mysql_query('SELECT @@ft_min_word_len as min, @@ft_max_word_len as max', $dblink);
	if ($r && ($word_lens = mysql_fetch_assoc($r))) {
		$CONFIG->search_info['min_chars'] = $word_lens['min'];
		$CONFIG->search_info['max_chars'] = $word_lens['max'];
	} else {
		// uhhh these are good numbers.
		$CONFIG->search_info['min_chars'] = 4;
		$CONFIG->search_info['max_chars'] = 90;
	}
*/
	$CONFIG->search_info['min_chars'] = PHP_INT_MAX;
	$CONFIG->search_info['max_chars'] = PHP_INT_MAX;
最小文字数をとりあえずINTの最大値にしておいて、常に LIKE 検索するようにします。
SQL文を確認。
SELECT count(DISTINCT e.guid) as total 
FROM entities e  JOIN objects_entity oe ON e.guid = oe.guid  
WHERE  (oe.title LIKE '%検索語%' OR oe.description LIKE '%検索語%')
 AND  ((e.type = 'object' AND e.subtype IN (4)))
 AND  (e.site_guid IN (1))
 AND ( (e.access_id = -2
                        AND e.owner_guid IN (
                                SELECT guid_one FROM entity_relationships
                                WHERE relationship='friend' AND guid_two=41
                        )) OR  (e.access_id IN (2,1,3)
                        OR (e.owner_guid = 41)
                        OR (
                                e.access_id = 0
                                AND e.owner_guid = 41
                        )
                ) and e.enabled='yes')
これで日本語での検索が出来るようになりました。

テーブルの方も当然全文検索インデックスが作成されるようになっています。
CREATE TABLE `groups_entity` (
  `guid` bigint(20) unsigned NOT NULL,
  `name` text NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY (`guid`),
  KEY `name` (`name`(50)),
  KEY `description` (`description`(50)),
  FULLTEXT KEY `name_2` (`name`,`description`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `objects_entity` (
  `guid` bigint(20) unsigned NOT NULL,
  `title` text NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY (`guid`),
  FULLTEXT KEY `title` (`title`,`description`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `sites_entity` (
  `guid` bigint(20) unsigned NOT NULL,
  `name` text NOT NULL,
  `description` text NOT NULL,
  `url` varchar(255) NOT NULL,
  PRIMARY KEY (`guid`),
  UNIQUE KEY `url` (`url`),
  FULLTEXT KEY `name` (`name`,`description`,`url`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `users_entity` (
  `guid` bigint(20) unsigned NOT NULL,
  `name` text NOT NULL,
  `username` varchar(128) NOT NULL DEFAULT '',
  `password` varchar(32) NOT NULL DEFAULT '',
  `salt` varchar(8) NOT NULL DEFAULT '',
  `email` text NOT NULL,
  `language` varchar(6) NOT NULL DEFAULT '',
  `code` varchar(32) NOT NULL DEFAULT '',
  `banned` enum('yes','no') NOT NULL DEFAULT 'no',
  `admin` enum('yes','no') NOT NULL DEFAULT 'no',
  `last_action` int(11) NOT NULL DEFAULT '0',
  `prev_last_action` int(11) NOT NULL DEFAULT '0',
  `last_login` int(11) NOT NULL DEFAULT '0',
  `prev_last_login` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`guid`),
  UNIQUE KEY `username` (`username`),
  KEY `password` (`password`),
  KEY `email` (`email`(50)),
  KEY `code` (`code`),
  KEY `last_action` (`last_action`),
  KEY `last_login` (`last_login`),
  KEY `admin` (`admin`),
  FULLTEXT KEY `name` (`name`),
  FULLTEXT KEY `name_2` (`name`,`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
全文検索を使用しないのであれば、これらのインデックスも削除しておけば余分なインデックス処理がなくなります。


コメントは受け付けていません。