C言語でCMSを書く その4 サーチエンジンとハッシュタグ

記事
IT・テクノロジー
WEBサイト内サーチエンジン
WEBサイトの中でも、ページ数が多くなってくれば、サーチエンジンでページを検索するようになるのは、まあ、当然です。とくに、NFTプラットフォームでは、多数のユーザがアカウントを持って行て、かつそのユーザが自分のブログとかコンテンツとかをどんどん投稿することになるから、ユーザ検索ができて、かつユーザごとにブログやコンテンツを検索できて、さらに、サーバ内にある全てのブログやコンテンツを一挙に検索できるような仕組みも必要です。

ところが、ブログやコンテンツといったもののテキストファイルを一挙に検索とかなると、それはそれで結構大変なので、一番良いのは、ブログやコンテンツのタイトル(表題)を検索対象にする、というもの。これだと、せいぜい数百文字の程度だから、多数のブログ、コンテンツからの検索もかなり高速で行けるわけです。逆にそうすると、ブログやコンテンツの文章内に、検索ワードをハッシュタグとして潜ませておけば、そこをクリックしたときに、そのハッシュタグを表題に含むブログやコンテンツの一覧を出してくれたりします。

また、ブログやコンテンツの題名にハッシュタグがいくつか入っているようにすると、同じハッシュタグが入っているブログやコンテンツが関連ブログ、みたいな形でスレッドを形成するようにもできます。

実は、 "#" の後ろに適当な長さの文字列を書いて、空白か改行までを一つのハッシュタグと見做す、みたいなのは実に理にかなっているもので、これで検索キーワードが明確に指定できるし、それに表題にたくさんのハッシュタグをいれておけば、それを全部検索ワードに指定できるので、ブログやコンテンツを分類することもできます。

検索エンジンの実装
AWExion HOTPortでは、連携している分散サーバや、登録されたアカウント、アカウント登録したユーザが投稿したタイトル(記事、コンテンツ、商品)は、id型の構造体でデータベース内に入っています。
----ここまで----
typedef struct          id
{
//128ビットのIDである Id128は、以下の内容です。
//分散サーバについては、IPアドレスから生成
//ユーザのIdは、32ビットずつ4つの部分に分かれ、
//上位32ビットはユーザ登録された最初のサーバのId
//次の32ビットはユーザ自身のId
//次の32ビットはユーザが現在所属しているサーバのId
//最後の32ビットはユーザのタイプ(管理者、会計担当者、ゲストなど)
//タイトル(記事、コンテンツ、商品)のIdの上位96ビットは、
//投稿したユーザのIdと共通。下位32ビットは、そのユーザの投稿した
//タイトルの通し番号
  uint128               Id128;
//連携しているサーバのURL、ユーザのログイン名(非公開)、
//タイトルの表題を表す文字列。
  char*                 IdString;
//連携しているサーバの登録順の通し番号、
//128ビットのユーザId128の上位64ビット
//128ビットのタイトルのId128の下位32ビットなどを格納
  int64                 IdNum;
};
----ここまで----
検索エンジンが行うのは、主に、id型構造体の中のId128, IdString, IdNumの検索で、とくに文字列型で定義されている idStringが検索対象になることが多いです。その場合は、文字列検索なのですが、これには、ごくごく普通に、C言語に標準である strcasestr()を使います。strstr()でも良いですが、英字の大文字小文字を区別しない strcasestr()のほうが、ヒットする率が多少は高くなりますよね。
 char *strcasestr(const char *haystack, const char *needle);
strcasestr()は、最初の haystack という引数で、検索対象となる文字列を与え、二番目のneedleで検索する文字列を与えます。つまり、needleという文字列が、haystackの中にあれば、発見されたことになり、返り値は、発見した場所の先頭のポインタをかえし、なければ、返り値はNULLになります。

この関数は大変便利ですが、対象となる haystackがNULL文字 '\0' で終了する文字列でなくては、なりません。バイナリの場合は'\0'に対応する0x00のバイトが中にあると、そこで終わってしまう、ということで、困るわけです。実は、CGIから丸投げされた HTTPのメッセージボディで multipart-formdata の中のセパレータ文字列を検索するのに strstrなど使っていたら、画像を入力するときに、大変なことになり、仕方無いので、binstr()という関数を自作しました。
短い関数なので、ここで紹介します。
----ここから----
//Buff 検索対象の領域の先頭
//BuffEnd 検索対象の領域の終端
//Key 検索する文字列
static  uint8*  binStr (uint8* Buff, uint8* BuffEnd, char*  Key)
{
//引数のどれか一つがNULLだったらNULLを返す。
  if   (NULL  ==Buff  ||
        NULL  ==BuffEnd       ||
        NULL   ==Key)                                   return  NULL;
  uint8*        Ptr            =Buff;
//先頭からスキャンするwhileループ
  while(Ptr    <BuffEnd-strlen (Key))
    {
//スキャンして先頭バイトが一致するかチェック
      if       (        Ptr    [0]    ==(uint8) Key    [0])
    {
//先頭バイトが一致したので、以降バイトの一致が続くまでチェック。
//一致したバイトの数をカウント
      int32 MatchCnt       =0;
      for  (size_t  I      =0;      I      <strlen  (Key);++I)
        { if       (Ptr    [I]    ==(uint8) Key    [I])   { MatchCnt      ++;}      else    break;}
//Keyで与えられた検索文字列の長さと、一致した文字列が同じになったら、
//発見されたとして、返す。
      if   (MatchCnt      ==strlen (Key))                                                   return  Ptr;
    }       Ptr   ++;
    }                                                                                           return  NULL;
}
----ここまで----
まあ、全く工夫もなにもない単純な関数ですが、たぶん、面倒なことをして速度を向上とか考えるよりは、これが一番速いんじゃないかと思います。strstrの実装なども、たぶんそうやっているはずです。

複数の文字列を同時検索する方法
さて、本論に戻ります。サーチエンジンは普通、一つの文字列を検索するだけでなく、複数の文字列を検索する必要があります。大概は、検索のためのフォームには、こんな感じで、複数の文字列を書くと、それらが全部入ったものがヒットする、というわけです。
SearchForm.png
この場合、検索文字列1、検索文字列2、検索文字列3は、スペースで区切られているのですが、スペースというのが厄介で、標準的な半角スペース(0x20)もあれば、日本語や中国語などの入力では全角文字列が複数あります。ですから、フォームから送られてくる文字列の中の全角スペースなどを、全部、標準的な半角スペースに変換する仕組みが必要です。ということで、検索用のフォームから送られてくるもの以外も、フォームでおくられてきた文字列については、全部、スペースは、半角スペースに置き換えるという処理をしておきます。
そうして、抽出された検索文字列は、char* SearchStrという文字列に一旦入れられ、それを、element型構造体に、一つ一つばらして入れます。
その関数が、以下の element_setSearchStrings()です。
----ここから----
static  bool    element_setSearchStrings       (threadManager*  ThMPtr,
                                                                       element*        This,
                                                                       char*           SearchStrings)
{ if   (FALSE ==Start  ("element_setSearchStrings"))           return  FALSE;
  if   (NULL  ==SearchStrings)                                              return  TRUE;
  int32         P              =0;
  while(isspace(SearchStrings  [P]))  ++P;
  if     ('\0'  ==SearchStrings  [P])             return  TRUE;
  char          SearchStr      [0x1000];
  while('\0'  !=SearchStrings  [P])
    {
      int32     Pt             =0;
//ここで、スペースのある場所を isspace()で探します。
      while    (isspace(SearchStrings  [P]))  ++P;
      while  ((!isspace(SearchStrings  [P]))&&  '\0'  !=SearchStrings  [P])
    { SearchStr    [Pt]    =SearchStrings  [P];   ++Pt;   ++P;}
      SearchStr[Pt]            ='\0';
      if       (0      <Pt)
    {
//あ、ここに、C99の表現がありました。
//C言語ではこういう書き方はできません。
      tagBlock      NewTB  =      {.HashTag        =NULL,
                                                 .UpdateTime     =time   (NULL),
                                                 .StructId      =&SID_string,
                                                 .Archive        =NULL,
                                                 .Block  .Ptr    =SearchStr};
      if   (FALSE ==element_addTagBlock    (ThMPtr,&ThMPtr->MM,     This,   FALSE,  NewTB)) continue;
    }
    }                                                                                           return  TRUE;
}
----ここまで----
ようするに、elementの中に、一つ一つの検索文字列を入れてやるだけです。単なる配列なので element->HashTagはNULLを入れておきます。
そうなれば、あとは、element型に入っている複数の検索文字列について、strcasestr()で検索すれば良いという話です。

HOTPortでは、いろいろなシーンで使える検索エンジンを利用するための関数ライブラリが結構揃っているので、それらを組み合わせると、一つのタイトル(記事、コンテンツ、商品)について、関連するタイトル(同じハッシュタグを含むものなど)を検索するのも容易です。このあたりがサックり簡単にできるのは、大変だったけれど、全部C言語で書いたからだと思っています。

まとめに変えて
ということで、今回は、これでおしまいです。ほぼ、これで基本的な部分のお話は終わったと思うので、まだ書いていなかった、multipart-formdataの処理とかそういうあたりを、次は書いていくつもりです。
こちらでご紹介した部分についても、以下のサービスでソースの提供を行います。
なお、HOTPortの全ソースを提供して、NFTプラットフォームを構築するサービスも開始しました。こちらも合わせて宜しくお願いいたします。

サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す