MySQLの標準機能で日本語を全文検索する(1)

MySQL には全文検索機能が付いている。だが、ラテン語のようにスペースがないと語句の区切りを認識しないため、そのままでは日本語が検索できない。 MySQLで全文検索 - FULLTEXTインデックスの基礎知識|blog|たたみラボ を参考に構築してみた。 データベースの文字コードは UTF-8 で、最低検索語の指定は1が本当はよいとは思うが少しでも節約したいので、2とした。 my.cnf [mysqld] ft_min_word_len=2 まず必要なのは、日本語の語句分解なのだが、形態素解析ではなく N-gram により分解する。但し、日本語(ひらがな、カタカナ、漢字)、英語(アルファベット、数字)への分解をまずすることで無駄なインデックス作成を抑制するようにしてみた。日本語に関しては N-gram を用いて、英語に関しては、キャメル記法に対しては、分解するようにした。 myindex.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 <?php mb\_internal\_encoding("UTF-8"); mb\_regex\_encoding("UTF-8"); class FullTextSearcher { var $markwords; var $scorequery; public function FullTextSearcher() { } public function search_sql($statement) { return "'". $this->to_ngram($statement, 2) ."' IN BOOLEAN MODE"; } public function getScore() { return "'".$this->scorequery."'"; } public function getMarkupWords() { return $this->markwords; } private function to_ngram($string, $n){ $string = mb\_ereg\_replace("^(\\s| )+","", $string); $string = mb\_ereg\_replace("(\\s| )+$","", $string); $str_array = preg_split("/(\\s| )+/", $string); $result = array(); $markwords = array(); $scores = array(); foreach ($str_array as $str){ if ('-' != substr($str, 0, 1)) { $markwords\[\] = $str; } $r = $this->to\_ngram\_query($str, $n); $result\[\] = $r\[0\]; $scores\[\] = $r\[1\]; } $this->markwords = $markwords; $this->scorequery = join(' ', $scores); return join(' ', $result); } private function to\_ngram\_query($string, $n){ $string = trim($string); if ($string == ''){ return ''; } if ('-' != substr($string, 0, 1)) { $plus = true; } else { $plus = false; $string = substr($string, 1); } $length = mb_strlen($string); if ($length < $n){ if ($plus) { return array("+".$string."*", $string); } else { return array("-".$string."*", ""); } } $ngrams = array(); $scores = array(); $wa = new WordsAnalyzer(); $wa->loadStr($string); $wds = $wa->get_indexes(); foreach ($wds as $ngram){ if ($plus) { $ngrams\[\] = "+" . $ngram; $scores\[\] = $ngram; } else { $ngrams\[\] = "-" . $ngram; } } return array(join(' ', $ngrams), join(' ', $scores)); } } class WordsAnalyzer { private $words; public function WordsAnalyzer() { } public function get_indexes() { return array_keys($this->words); } public function get_fulltext() { return join(' ', array_keys($this->words)); } public function loadStr($str) { $this->words = array(); ereg_replace(13, "", $str); ereg_replace(10, " ", $str); $this->analyze_token($this->split_token($str)); } private function split_token($str) { $token = array(); while (1) { $bytes = mb_ereg("\[一-龠\]+|\[ぁ-ん\]+|\[ァ-ヴー\]+|\[a-zA-Z0-9\]+|\[a-zA-Z0-9\]+", $str, $match); if ($bytes == FALSE) break; $match = $match\[0\]; array_push($token, $match); $pos = strpos($str, $match); $str = substr($str, $pos+$bytes); } return $token; } private function analyze_token($token) { for ($i = 0, $len = count($token); $i < $len; $i++) { $word = $token\[$i\]; if (mb_ereg("\[一-龠\]", $word, $match) != FALSE) { if (mb_strlen($word) > 1) { $this->japanese($word); } else { $this->addword($word); } } else if (mb_ereg("\[ぁ-ん\]", $word, $match) != FALSE) { if (mb_strlen($word) > 1) { $this->hiragana($word); } } else if (mb_ereg("\[ァ-ヴー\]", $word, $match) != FALSE) { if (mb_strlen($word) > 1) { $this->japanese($word); } } else if (mb_ereg("\[a-zA-Z0-9\]", $word, $match) != FALSE) { if (strlen($word) > 0) { // camel-gram $this->english($word); } } else if (mb_ereg("\[a-zA-Z0-9\]", $word, $match) != FALSE) { if (mb_strlen($word) > 0) { // camel-gram $word = mb\_convert\_kana($word, 'rn'); $this->english($word); } } else { $this->addword($word); } } } private function addword($word) { if (strlen($word) == 0) return; $this->words{$word} = true; } private function japanese($str) { // bi-gram $ret = $this->split_ngram($str, 2); foreach ($ret as $key => $value) { if (mb_strlen($value) > 1) { $this->addword($value); } } } private function hiragana($str) { // bi-gram $ret = $this->split_ngram($str, 2); foreach ($ret as $key => $value) { if (mb_strlen($value) > 1) { if (mb_ereg("っ.|を|です|ます|する|ある|いる|から", $value, $match) != FALSE) { } else { $this->addword($value); } } } } private function english($str) { $this->addword($str); // そのままも登録 // キャメル記法を分解して登録 while(1){ $r = ereg("(\[a-z\]|\[A-z\]|\[0-9\])\[a-z\]+", $str, $match); if ($r == FALSE) { break; } $match = $match\[0\]; $this->addword($match); $pos = strpos($str, $match); $str = substr($str, $pos + $r); } } private function split_ngram($string, $n){ $ngrams = array(); $string = trim($string); if ($string == '') return ''; for ($i = 0, $len = mb_strlen($string); $i < $len; $i++) { $ngrams\[\] = mb_substr($string, $i, $n); } return $ngrams; } } ?>

2007年4月25日 · Toshimitsu Takahashi

.NET メールのDateフィールド値をDateTimeに変換

下記のような、メールヘッダのDate値を変換する。 Date: Sun, 02 Jul 2006 19:45:13 +0100 CultureInfoを en-US にしないと、曜日、月の省略語を正しく認識できない。 // 変換に失敗すると FormatException を投げる。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static DateTime ParseMailDate(string field){ // (JST) などを取り払う int i = field.LastIndexOf("("); if (i > -1){ field = field.Substring(0, i).TrimEnd(); } string[] expectedFormats = {"ddd, d MMM yyyy HH':'mm':'ss zzz", "ddd, d MMM yyyy H':'m':'s zzz", "d MMM yyyy HH':'mm':'ss zzz", "ddd, d MMM yyyy HH':'mm':'ss", "ddd, d MMM yyyy H':'m':'s", "d MMM yyyy HH':'mm':'ss"}; // ゴミがあったら削除 if (field.Length > 31) { field = field.Substring(0, field.LastIndexOf(' ') - 1); } return System.DateTime.ParseExact(field, expectedFormats, System.Globalization.CultureInfo.GetCultureInfo("en-US"), System.Globalization.DateTimeStyles.None); }

2007年4月24日 · Toshimitsu Takahashi

XSLT を用いて XML の属性名をもとにノード名を付け替える

マッピングが定義されたXMLを使って、itemノードのname属性値から対応するタグ名に変換する。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" encoding="UTF-8"/> <xsl:variable name="convtable" select="document('mapping.xml')"/> <xsl:template match="/records"> <xsl:copy> <xsl:apply-templates select="record"/> </xsl:copy> </xsl:template> <xsl:template match="record"> <xsl:copy> <xsl:apply-templates select="item"/> </xsl:copy> </xsl:template> <xsl:template match="item"> <xsl:variable name="oldname" select="@name"/> <xsl:variable name="newname" select="$convtable/mapping/item\[@from=$oldname\]/@to"/> <xsl:choose> <xsl:when test="$newname"> <xsl:element name="{$newname}"> <xsl:value-of select="."/> </xsl:element> </xsl:when> <xsl:otherwise> <xsl:copy-of select="."/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet> mapping.xml 1 2 3 4 5 <?xml version="1.0" encoding="utf-8"?> <mapping> <item from="タイトル" to="title"/> <item from="著者名" to="author"/> </mapping> 入力 1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="utf-8"?> <records> <record> <item name="タイトル">はてな</item> <item name="著者名">スタッフ</item> </record> <record> <item name="タイトル">小説</item> <item name="著者名">誰かさん</item> </record> </records> 結果 1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="utf-8"?> <records> <record> <title>はてな</title> <author>スタッフ</author> </record> <record> <title>小説</title> <author>誰かさん</author> </record> </records>

2007年4月10日 · Toshimitsu Takahashi

PHP XSLT 変換出力サンプル

sample.xsl をロードした [XSLT で test.xml を変換して出力する。 1 2 3 4 5 6 7 8 9 10 <?php header('Content-Type:text/xml;charset=utf-8'); $doc = new DOMDocument(); $doc->load('test.xml'); $docxsl = new DOMDocument(); $docxsl->load('sample.xsl'); $xslt = new XsltProcessor(); $xslt->importStylesheet($docxsl); echo $xslt->transformToXML($doc); ?>

2007年4月9日 · Toshimitsu Takahashi

MySQL - GRANT ステートメント

管理ユーザの追加。管理ユーザ myadmin を追加している。IDENTIFIED BY の後がパスワードとなる。なお、"%" にはlocalhost が含まれないため必要となる。 # ./mysql -u root mysql> GRANT ALL PRIVILEGES ON *.* TO myadmin@localhost IDENTIFIED BY 'myadmin' WITH GRANT OPTION; mysql> GRANT ALL PRIVILEGES ON *.* TO myadmin@"%" IDENTIFIED BY 'myadmin' WITH GRANT OPTION;

2007年4月5日 · Toshimitsu Takahashi

Solaris 10 で libstdc++.la が空

コンパイルエラーで気づいた。下記を書き込んでしまう。SPARC用。 http://forum.java.sun.com/thread.jspa?threadID=5073150 /usr/sfw/lib/libstdc++.la # libstdc++.la - a libtool library file # Generated by ltmain.sh - GNU libtool 1.4a-GCC3.0 (1.641.2.256 2001/05/28 20:09:07 with GCC-local changes) # # Please DO NOT delete this file! # It is necessary for linking the library. # The name that we can dlopen(3). dlname='libstdc++.so.6' # Names of this library. library_names='libstdc++.so.6.0.3 libstdc++.so.6 libstdc++.so' # The name of the static archive. old_library='libstdc++.a' # Libraries that this one depends upon. dependency\_libs='-lc -lm -L/usr/sfw/lib -lgcc\_s' # Version information for libstdc++. current=6 age=0 revision=3 # Is this an already installed library? installed=yes # Files to dlopen/dlpreopen dlopen='' dlpreopen='' # Directory that this library needs to be installed in: libdir='/usr/sfw/lib' /usr/sfw/lib/64/libstdc++.la # libstdc++.la - a libtool library file # Generated by ltmain.sh - GNU libtool 1.4a-GCC3.0 (1.641.2.256 2001/05/28 20:09:07 with GCC-local changes) # # Please DO NOT delete this file! # It is necessary for linking the library. # The name that we can dlopen(3). dlname='libstdc++.so.6' # Names of this library. library_names='libstdc++.so.6.0.3 libstdc++.so.6 libstdc++.so' # The name of the static archive. old_library='libstdc++.a' # Libraries that this one depends upon. dependency\_libs='-L/lib/64 -lc -lm -L/usr/sfw/lib/64 -lgcc\_s' # Version information for libstdc++. current=6 age=0 revision=3 # Is this an already installed library? installed=yes # Files to dlopen/dlpreopen dlopen='' dlpreopen='' # Directory that this library needs to be installed in: libdir='/usr/sfw/lib/64'

2007年3月28日 · Toshimitsu Takahashi

.NET 1.1 で COM ポイントに接続

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private struct WindowData { public System.Windows.Forms.HtmlDocument document; public IConnectionPoint icpWin; public int cookieWin; public IConnectionPoint icpDoc; public int cookieDoc; } private WindowData setWindowComConnect(object pDisp){ WindowData data = new WindowData(); // UCOM Connect Guid guid; data.document = (HTMLDocument)((SHDocVw.IWebBrowser2)pDisp).Document; // Document IConnectionPointContainer pConPtCon = (IConnectionPointContainer)data.document; guid = typeof(HTMLDocumentEvents2).GUID; pConPtCon.FindConnectionPoint(ref guid, out data.icpDoc); data.icpDoc.Advise(this, out data.cookieDoc); // Window IConnectionPointContainer pCpcWin = (IConnectionPointContainer)data.document.parentWindow; guid = typeof(HTMLWindowEvents2).GUID; pCpcWin.FindConnectionPoint(ref guid, out data.icpWin); data.icpWin.Advise(this, out data.cookieWin); return data; } private void unsetWindowComConnect(WindowData data){ if (data.icpWin != null) { try { data.icpWin.Unadvise(data.cookieWin); Marshal.ReleaseComObject(data.icpWin); } catch (Exception ex) { } } if (data.icpDoc != null) { data.icpDoc.Unadvise(data.cookieDoc); Marshal.ReleaseComObject(data.icpDoc); } if (data.document != null) { Marshal.ReleaseComObject(data.document); } }

2007年3月25日 · Toshimitsu Takahashi

MySQL SQLシェル

まず、シェルを起動。-p でパスワード入力をする。 $ mysql -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 463 to server version: 5.0.20a-log Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the buffer. データベースを列挙する mysql> show databases; +——————–+ | Database | +——————–+ | mt | | mysql | +——————–+ 8 rows in set (0.00 sec) データベースを選択する mysql> use mt; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed ...

2007年3月19日 · Toshimitsu Takahashi

Mac OS X サービスを自動起動する

Mac OS X でのサービスの起動・停止スクリプトを登録してマシンの起動・停止と同期させる方法。 ユーザがインストールしたアプリは、/Library/StartupItems に アプリのディレクトリを掘る drwxr-xr-x 4 root wheel 136 Nov 26 23:16 MySQL その下にスクリプト(MySQL)とStartupParameters.plist(XMLではない表記も可能) ファイルを置く。 MySQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #!/bin/sh # \# /Library/StartupItems/MySQL/MySQL # \# A script to automatically start up MySQL on system bootup \# for Mac OS X. This is actually just a wrapper script around \# the standard mysql.server init script, which is included in \# the binary distribution. # \# (c) 2003 MySQL AB \# Written by Lenz Grimmer <lenz@mysql.com> # \# Suppress the annoying "$1: unbound variable" error when no option \# was given if \[ -z $1 \] ; then echo "Usage: $0 \[start|stop|restart\] " exit 1 fi \# Source the common setup functions for startup scripts test -r /etc/rc.common || exit 1 . /etc/rc.common \# The path to the mysql.server init script. The official MySQL \# Mac OS X packages are being installed into /usr/local/mysql. SCRIPT="/usr/local/mysql5/share/mysql/mysql.server" StartService () { if \[ "${MYSQLCOM:=-NO-}" = "-YES-" \] ; then ConsoleMessage "Starting MySQL database server" $SCRIPT start > /dev/null 2>&1 fi } StopService () { ConsoleMessage "Stopping MySQL database server" $SCRIPT stop > /dev/null 2>&1 } RestartService () { ConsoleMessage "Restarting MySQL database server" $SCRIPT restart > /dev/null 2>&1 } if test -x $SCRIPT ; then RunService "$1" else ConsoleMessage "Could not find MySQL startup script!" fi StartupParameters.plist ...

2007年3月18日 · Toshimitsu Takahashi

Excel の XML スプレッドシート形式を XSLT で一般的な XML に整形する

1行目を列名とする。 book → シート名 → row → 列名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?xml version="1.0" encoding="Shift_JIS"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/"> <xsl:apply-templates select="Workbook" /> </xsl:template> <!-- ブックの書き出し --> <xsl:template match="Workbook"> <book> <xsl:apply-templates select="Worksheet" /> </book> </xsl:template> <!-- シートの書き出し --> <xsl:template match="Worksheet"> <xsl:element name="@Name"> <xsl:apply-templates select="Table/Row"> <xsl:with-param name="sheetName" select="@Name"/> </xsl:apply-templates> </xsl:element> </xsl:template> <xsl:template match="Row\[position()=1\]"> </xsl:template> <!-- 行の書き出し --> <xsl:template match="Row"> <xsl:param name="sheetName" /> <row> <xsl:apply-templates select="Cell"> <xsl:with-param name="sheetName" select="$sheetName"/> </xsl:apply-templates> </row> </xsl:template> <!-- セルの書き出し --> <xsl:template match="Cell"> <xsl:param name="sheetName" /> <xsl:variable name="rowIndex" select="position()"/> <xsl:variable name="nodename" select="/Workbook/Worksheet\[@Name=$sheetName\]/Table/Row\[1\]/Cell\[$rowIndex\]/Data"/> <xsl:variable name="data" select="Data"/> <xsl:if test="$nodename!=''"> <xsl:if test="$data!=''"> <xsl:element name="{$nodename}"> <xsl:value-of select="$data" /> </xsl:element> </xsl:if> </xsl:if> </xsl:template> </xsl:stylesheet>

2007年3月15日 · Toshimitsu Takahashi