Просмотр исходного кода

Merge pull request #1680 from visuddhinanda/laravel

加入巴利文搜索微服务支持
visuddhinanda 2 лет назад
Родитель
Сommit
45aa4a1d91

+ 93 - 0
app/Console/Commands/ExportFtsPali.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\DictApi;
+use App\Models\UserDict;
+
+class ExportFtsPali extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:fts.pali';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出全文搜索用的巴利语词汇表';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        //irregular
+        $dictId = ['4d3a0d92-0adc-4052-80f5-512a2603d0e8'];
+         //regular
+        $dictId[] = DictApi::getSysDict('system_regular');
+        $long = ["ā","ī","ū"];
+        $path = storage_path('app/export/fts');
+        if(!is_dir($path)){
+            $res = mkdir($path,0700,true);
+            if(!$res){
+                Log::error('mkdir fail path='.$exportDir);
+                return 1;
+            }
+        }
+        $fp = fopen($path.'/pali.syn','w') or die("Unable to open file!");
+        $count=0;
+        foreach ($dictId as $key => $value) {
+            $words = UserDict::where('dict_id',$value)
+                             ->select('word')
+                             ->groupBy('word')->cursor();
+            $this->info('word count='.count($words));
+            foreach ($words as $key => $word) {
+                $count++;
+                if($count % 1000 === 0){
+                    $this->info($count);
+                }
+                $parent = UserDict::where('dict_id',$value)
+                             ->where('word',$word->word)
+                             ->selectRaw('parent,char_length("parent")')
+                             ->groupBy('parent')->orderBy('char_length','asc')->first();
+
+                if($parent && !empty($parent->parent)){
+                    $end = mb_substr($parent->parent,-1,null,"UTF-8");
+                    if(in_array($end,["ā","ī","ū"])){
+                        $head = mb_substr($parent->parent,0,mb_strlen($parent->parent)-1,"UTF-8");
+                        $newEnd = str_replace(["ā","ī","ū"],["a","i","u"],$end);
+                        $parentWord = $head.$newEnd;
+
+                    }else{
+                        $parentWord = $parent->parent;
+                    }
+                    fwrite($fp, $word->word.' '.$parentWord.PHP_EOL);
+                }else{
+                    $this->error('word no parent word='.$word->word);
+                }
+            }
+        }
+        fclose($fp);
+
+
+        return 0;
+    }
+}

+ 4 - 4
app/Console/Commands/MqDiscussion.php

@@ -54,7 +54,7 @@ class MqDiscussion extends Command
         $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
         Log::info("discussion worker start .");
         Mq::worker($exchange,$queue,function ($message){
-            Log::info('mq discussion start {message}',['message'=>json_encode($message,JSON_UNESCAPED_UNICODE)]);
+            Log::info('mq discussion receive {message}',['message'=>json_encode($message,JSON_UNESCAPED_UNICODE)]);
             $result = 0;
             switch ($message->res_type) {
                 case 'sentence':
@@ -97,7 +97,7 @@ class MqDiscussion extends Command
                                   ->where('title',$articleTitle)
                                   ->value('content');
                     if(empty($tpl)){
-                        Log::error('mq:pr 模版不能为空',['tpl_title'=>$articleTitle]);
+                        Log::error('mq:discussion 模版不能为空',['tpl_title'=>$articleTitle]);
                         return 1;
                     }
                     $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES,
@@ -140,10 +140,10 @@ class MqDiscussion extends Command
                         }
 
                         if($ok === 0){
-                            Log::debug('mq:pr: success {url}',['url'=>$hook->url]);
+                            Log::debug('mq:discussion: send success {url}',['url'=>$hook->url]);
                             WebHook::where('id',$hook->id)->increment('success');
                         }else{
-                            Log::error('mq:pr: success {url}',['url'=>$hook->url]);
+                            Log::error('mq:discussion: send fail {url}',['url'=>$hook->url]);
                             WebHook::where('id',$hook->id)->increment('fail');
                         }
                     }

+ 55 - 0
app/Console/Commands/TestSearchPali.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Tools\PaliSearch;
+
+class TestSearchPali extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:search.pali';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        //$result = PaliSearch::search(['citta'],[93,94],'case',0,10);
+        //var_dump($result);
+        //$result = PaliSearch::book_list(['citta'],[93,94],'case',0,10);
+        //var_dump($result);
+        //$this->info(PaliSearch::upload_dict('ddd'));
+        $full = '19 . ‘ ‘ ‘ Yathā vā paneke bhonto samaṇabrāhmaṇā saddhādeyyāni bhojanāni bhuñjitvā te evarūpaṃ dūteyyapahiṇagamanānuyogaṃ anuyuttā viharanti , seyyathidaṃ – raññaṃ , rājamahāmattānaṃ , khattiyānaṃ , brāhmaṇānaṃ , gahapatikānaṃ , kumārānaṃ ‘ ‘ idha gaccha , amutrāgaccha , idaṃ hara , amutra idaṃ āharā ’ ’ ti iti vā iti evarūpā dūteyyapahiṇagamanānuyogā paṭivirato samaṇo gotamo ’ ti – iti vā hi , bhikkhave , puthujjano tathāgatassa vaṇṇaṃ vadamāno vadeyya . ';
+        $test = 'bhonto samaṇabrāhmaṇā';
+        $update = PaliSearch::update(93,50,
+                                        '','','',
+                                        $test,
+                                        99);
+        $this->info($update);
+        return 0;
+    }
+}

+ 69 - 24
app/Console/Commands/UpgradeFts.php

@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
 use App\Models\BookTitle;
 use App\Models\FtsText;
 use App\Models\WbwTemplate;
+use App\Tools\PaliSearch;
 
 class UpgradeFts extends Command
 {
@@ -14,9 +15,9 @@ class UpgradeFts extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:fts {--content : upgrade col content}
+    protected $signature = 'upgrade:fts {--content : upgrade col content only}
         {para?}
-        {--test : output log}';
+        {--test : output log only}';
 
     /**
      * The console command description.
@@ -45,36 +46,80 @@ class UpgradeFts extends Command
         if(\App\Tools\Tools::isStop()){
             return 0;
         }
-        if($this->option('content')){
-            if(!empty($this->argument('para'))){
-                $para = explode('-',$this->argument('para'));
+
+        if(!empty($this->argument('para'))){
+            $para = explode('-',$this->argument('para'));
+        }
+        for ($iBook=1; $iBook <= 217; $iBook++) {
+            if(isset($para[0]) && $para[0] != $iBook){
+                continue;
             }
-            for ($iBook=1; $iBook <= 217; $iBook++) {
-                if(isset($para[0]) && $para[0] != $iBook){
+            # code...
+            $this->info('book:'.$iBook);
+            $maxParagraph = WbwTemplate::where('book',$iBook)->max('paragraph');
+            $bar = $this->output->createProgressBar($maxParagraph-1);
+            for($iPara=1; $iPara <= $maxParagraph; $iPara++){
+                if(isset($para[1]) && $para[1] != $iPara){
+                    $bar->advance();
                     continue;
                 }
-                # code...
-                $this->info('book:'.$iBook);
-                $maxParagraph = WbwTemplate::where('book',$iBook)->max('paragraph');
-                $bar = $this->output->createProgressBar($maxParagraph-1);
-                for($iPara=1; $iPara <= $maxParagraph; $iPara++){
-                    if(isset($para[1]) && $para[1] != $iPara){
-                        $bar->advance();
-                        continue;
-                    }
-                    $content = $this->getContent($iBook,$iPara);
-                    if($this->option('test')){
-                        $this->info($content);
+                $content = $this->getContent($iBook,$iPara);
+                //查找黑体字
+                $words = WbwTemplate::where('book',$iBook)
+                                    ->where('paragraph',$iPara)
+                                    ->orderBy('wid')->get();
+                $bold1 = array();
+                $bold2 = array();
+                $bold3 = array();
+                $currBold = array();
+                foreach ($words as $word) {
+                    if($word->style==='bld'){
+                        $currBold[] = $word->real;
                     }else{
-                        //TODO update bold
-                        FtsText::where('book',$iBook)->where('paragraph',$iPara)->update(['content'=>$content]);
+                        $countBold = count($currBold);
+                        if($countBold === 1){
+                            $bold1[] = $currBold[0];
+                        }else if($countBold === 2){
+                            $bold2 = array_merge($bold2,$currBold);
+                        }else if($countBold > 0){
+                            $bold3 = array_merge($bold3,$currBold);
+                        }
+                        $currBold = [];
                     }
-                    $bar->advance();
                 }
-                $bar->finish();
-                $this->info('done');
+                $pcd_book = BookTitle::where('book',$iBook)
+                        ->where('paragraph','<=',$iPara)
+                        ->orderBy('paragraph','desc')
+                        ->first();
+                if($pcd_book){
+                    $pcd_book_id = $pcd_book->sn;
+                }else{
+                    $pcd_book_id = BookTitle::where('book',$iBook)
+                                            ->orderBy('paragraph')
+                                            ->value('sn');
+                }
+                if($this->option('test')){
+                    $this->info($content.
+                                ' pcd_book='.$pcd_book_id.
+                                ' bold1='.implode(' ',$bold1).
+                                ' bold2='.implode(' ',$bold2).
+                                ' bold3='.implode(' ',$bold3).PHP_EOL
+                                );
+                }else{
+                    $update = PaliSearch::update($iBook,
+                                                 $iPara,
+                                                implode(' ',$bold1),
+                                                implode(' ',$bold2),
+                                                implode(' ',$bold3),
+                                                $content,
+                                                $pcd_book_id);
+                }
+                $bar->advance();
             }
+            $bar->finish();
+            $this->info('done');
         }
+
         return 0;
     }
 

+ 2 - 1
app/Http/Api/Mq.php

@@ -24,11 +24,12 @@ class Mq
         $connection = new AMQPStreamConnection($host, $port, $user, $password, $vhost);
         return $connection;
     }
+
     public static function publish(string $channelName, $message)
     {
         //一对一
         try {
-            Log::debug('mq start {channel} {message}', ['channel' => $channelName, 'message' => $message]);
+            Log::debug('mq publish {channel} {message}', ['channel' => $channelName, 'message' => $message]);
             $host = config("queue.connections.rabbitmq.host");
             $port = config("queue.connections.rabbitmq.port");
             $user = config("queue.connections.rabbitmq.user");

+ 87 - 23
app/Http/Controllers/CorpusController.php

@@ -13,6 +13,8 @@ use App\Models\Wbw;
 use App\Models\Discussion;
 use App\Models\PaliSentence;
 use App\Models\SentSimIndex;
+use App\Models\CustomBookSentence;
+
 use Illuminate\Support\Str;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
@@ -118,13 +120,18 @@ class CorpusController extends Controller
         if(count($sentId) !== 4){
             return false;
         }
-        if($mode==='read'){
-            $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+        if((int)$sentId[0] < 1000){
+            if($mode==='read'){
+                $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+            }else{
+                $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+            }
         }else{
-            $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+
         }
 
-        if($channelId !== false){
+
+        if(isset($channelId) && $channelId){
             array_push($channels,$channelId);
         }
         $record = Sentence::select($this->selectCol)
@@ -137,6 +144,55 @@ class CorpusController extends Controller
 
         $channelIndex = $this->getChannelIndex($channels);
 
+        if((int)$sentId[0] >= 1000){
+            $orgChannelId = (string)Str::uuid();
+            //载入自定义原文
+            $customOrigin = CustomBookSentence::
+                              where('book',(int)$sentId[0])
+                            ->where('paragraph',(int)$sentId[1])
+                            ->where('word_start',(int)$sentId[2])
+                            ->where('word_end',(int)$sentId[3])
+                            ->get();
+            $toSentFormat = array();
+            $owner_uid = null;
+            foreach ($customOrigin as $custom) {
+                if($owner_uid === null){
+                    $owner_uid = $custom->owner;
+                }
+                $toSentFormat[] = (object)[
+                    'uid'=>$custom->id,
+                    'book_id'=>$custom->book,
+                    'paragraph'=>$custom->paragraph,
+                    'word_start'=>$custom->word_start,
+                    "word_end"=>$custom->word_end,
+                    'channel_uid'=>$orgChannelId,
+                    'content'=>$custom->content,
+                    'content_type'=>$custom->content_type,
+                    'editor_uid'=>$custom->owner,
+                    'acceptor_uid'=>null,
+                    'pr_edit_at'=>null,
+                    'create_time'=>$custom->create_time,
+                    'modify_time'=>$custom->modify_time,
+                    'created_at'=>$custom->created_at,
+                    'updated_at'=>$custom->updated_at,
+                ];
+            }
+            $orgChannel = (object)array(
+                        'uid'=>$orgChannelId,
+                        'type'=>'original',
+                        'name'=>'custom',
+                        'owner_uid'=>$owner_uid,
+                        'studio'=>StudioApi::getById($owner_uid),
+                    );
+            $channelIndex[$orgChannelId] = $orgChannel;
+        }
+
+        if(isset($toSentFormat)){
+            foreach ($toSentFormat as $key => $org) {
+                $record[] = $org;
+            }
+        }
+
         //获取wbw channel
         //目前默认的 wbw channel 是第一个translation channel
         foreach ($channels as  $channel) {
@@ -528,7 +584,7 @@ class CorpusController extends Controller
         }
         foreach ($indexChannel as $uid => $value) {
             # 查询studio
-            $indexChannel[$uid]['studio'] = StudioApi::getById($value->owner_uid);
+            $indexChannel[$uid]->studio = StudioApi::getById($value->owner_uid);
         }
         return $indexChannel;
     }
@@ -552,7 +608,7 @@ class CorpusController extends Controller
         foreach ($record as $key => $value) {
             $currSentId = "{$value->book_id}-{$value->paragraph}-{$value->word_start}-{$value->word_end}";
             $sentList[$currSentId]=[$value->book_id ,$value->paragraph,$value->word_start,$value->word_end];
-            $value['sid'] = "{$currSentId}_{$value->channel_uid}";
+            $value->sid = "{$currSentId}_{$value->channel_uid}";
         }
         $channelsId = array();
         $count = 0;
@@ -592,6 +648,11 @@ class CorpusController extends Controller
             foreach ($indexChannel as $channelId => $info) {
                 # code...
                 $sid = "{$currSentId}_{$channelId}";
+                if(isset($info->studio)){
+                    $studioInfo = $info->studio;
+                }else{
+                    $studioInfo = null;
+                }
                 $newSent = [
                     "content"=>"",
                     "html"=> "",
@@ -604,13 +665,13 @@ class CorpusController extends Controller
                         "type"=>$info->type,
                         "id"=> $info->uid,
                     ],
-                    "studio" => $info['studio'],
+                    "studio" => $studioInfo,
                     "updateAt"=> "",
                     "suggestionCount" => SuggestionApi::getCountBySent($arrSentId[0],$arrSentId[1],$arrSentId[2],$arrSentId[3],$channelId),
                 ];
 
                 $row = Arr::first($record,function($value,$key) use($sid){
-                    return $value['sid']===$sid;
+                    return $value->sid===$sid;
                 });
                 if($row){
                     $newSent['id'] = $row->uid;
@@ -885,21 +946,24 @@ class CorpusController extends Controller
 			"translation"=>[],
 		];
 
-		#生成channel 数量列表
-		$sentId = "{$book}-{$para}-{$word_start}-{$word_end}";
-        $channelCount = CorpusController::_sentCanReadCount($book,$para,$word_start,$word_end,$this->userUuid);
-        $path = json_decode(PaliText::where('book',$book)->where('paragraph',$para)->value("path"),true);
-        $sent["path"] = [];
-        foreach ($path as $key => $value) {
-            # code...
-            $value['paliTitle'] = $value['title'];
-            $sent["path"][] = $value;
-        }
-		$sent["tranNum"] = $channelCount['tranNum'];
-		$sent["nissayaNum"] = $channelCount['nissayaNum'];
-		$sent["commNum"] = $channelCount['commNum'];
-		$sent["originNum"] = $channelCount['originNum'];
-		$sent["simNum"] = $channelCount['simNum'];
+        if($book<1000){
+            #生成channel 数量列表
+            $sentId = "{$book}-{$para}-{$word_start}-{$word_end}";
+            $channelCount = CorpusController::_sentCanReadCount($book,$para,$word_start,$word_end,$this->userUuid);
+            $path = json_decode(PaliText::where('book',$book)->where('paragraph',$para)->value("path"),true);
+            $sent["path"] = [];
+            foreach ($path as $key => $value) {
+                # code...
+                $value['paliTitle'] = $value['title'];
+                $sent["path"][] = $value;
+            }
+            $sent["tranNum"] = $channelCount['tranNum'];
+            $sent["nissayaNum"] = $channelCount['nissayaNum'];
+            $sent["commNum"] = $channelCount['commNum'];
+            $sent["originNum"] = $channelCount['originNum'];
+            $sent["simNum"] = $channelCount['simNum'];
+        }
+
 		return $sent;
 	}
 

+ 42 - 17
app/Http/Controllers/SearchController.php

@@ -17,6 +17,7 @@ use Illuminate\Support\Facades\Log;
 use App\Tools\Tools;
 use App\Models\WbwTemplate;
 use App\Models\PageNumber;
+use App\Tools\PaliSearch;
 
 
 class SearchController extends Controller
@@ -35,6 +36,7 @@ class SearchController extends Controller
                     return $this->page($request);
                 }else{
                     return $this->pali($request);
+                    //return $this->pali_rpc($request);
                 }
                 break;
             case 'page':
@@ -73,25 +75,26 @@ class SearchController extends Controller
     public function pali(Request $request)
     {
         //
-        $searchChapters = [];
-        $searchBooks = [];
-        $searchBookId = [];
-        $queryBookId = '';
-
+        $bookId = [];
         if($request->has('book')){
-            $queryBookId = ' AND pcd_book_id = ' . (int)$request->get('book');
+            $bookId = [(int)$request->get('book')];
         }else if($request->has('tags')){
             //查询搜索范围
             //查询搜索范围
             $tagItems = explode(';',$request->get('tags'));
-            $bookId = [];
+
             foreach ($tagItems as $tagItem) {
-                # code...
                 $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
             }
-            $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
         }
 
+        $searchChapters = [];
+        $searchBooks = [];
+        $searchBookId = [];
+        $queryBookId = '';
+        if(count($bookId) > 0){
+            $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
+        }
         $key = explode(';',$request->get('key')) ;
         $param = [];
         $countParam = [];
@@ -164,12 +167,32 @@ class SearchController extends Controller
 
         $result = DB::select($query, $param);
 
-        //待查询单词列表
-        //$caseMan = new CaseMan();
-        //$wordSpell = $caseMan->BaseToWord($key);
-
         return $this->ok(["rows"=>SearchResource::collection($result),"count"=>$resultCount[0]->co]);
     }
+    public function pali_rpc(Request $request)
+    {
+        //
+        $bookId = [];
+        if($request->has('book')){
+            $bookId = [(int)$request->get('book')];
+        }else if($request->has('tags')){
+            //查询搜索范围
+            //查询搜索范围
+            $tagItems = explode(';',$request->get('tags'));
+
+            foreach ($tagItems as $tagItem) {
+                $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
+            }
+        }
+
+        $key = explode(';',$request->get('key')) ;
+        $limit = $request->get('limit',10);
+        $offset = $request->get('offset',0);
+        $matchMode = $request->get('match','case');
+        $result = PaliSearch::search($key,$bookId,$matchMode,$offset,$limit);
+        return $this->ok(["rows"=>SearchResource::collection(collect($result['rows'])),"count"=>$result['total']]);
+    }
+
     public function page(Request $request)
     {
         //
@@ -223,10 +246,11 @@ class SearchController extends Controller
         $searchBooks = [];
         $queryBookId = '';
 
+        $bookId = [];
         if($request->has('tags')){
             //查询搜索范围
             $tagItems = explode(';',$request->get('tags'));
-            $bookId = [];
+
             foreach ($tagItems as $tagItem) {
                 # code...
                 $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
@@ -242,10 +266,11 @@ class SearchController extends Controller
                     $queryWhere = "type='.ctl.' AND word = ?";
                     $query = "SELECT pcd_book_id, count(*) as co FROM wbw_templates WHERE {$queryWhere} {$queryBookId} GROUP BY pcd_book_id ORDER BY co DESC;";
                     $result = DB::select($query, [$key]);
+
                 }else{
-                    $queryWhere = $this->getQueryWhere($key,$request->get('match','case'));
-                    $query = "SELECT pcd_book_id, count(*) as co FROM fts_texts WHERE {$queryWhere['query']} {$queryBookId} GROUP BY pcd_book_id ORDER BY co DESC;";
-                    $result = DB::select($query, $queryWhere['param']);
+                    $rpc_result = PaliSearch::book_list(explode(';',$key) ,$bookId,
+                                                    $request->get('match','case'));
+                    $result = collect($rpc_result['rows']);
                 }
                 break;
             case 'page':

+ 120 - 0
app/Tools/PaliSearch.php

@@ -0,0 +1,120 @@
+<?php
+namespace App\Tools;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+
+class PaliSearch
+{
+    public static function search($words,$books,$matchMode='case',$index=0,$size=10){
+        $host = config('mint.server.rpc.tulip');
+        Log::debug('tulip host='.$host);
+        $client = new \Mint\Tulip\V1\SearchClient($host, [
+            'credentials' => \Grpc\ChannelCredentials::createInsecure(),
+        ]);
+
+        $request = new \Mint\Tulip\V1\SearchRequest();
+        $request->setKeywords($words);
+        $request->setBooks($books);
+        $request->setMatchMode($matchMode);
+        $page = new \Mint\Tulip\V1\SearchRequest\Page;
+        $page->setIndex($index);
+        $page->setSize($size);
+        $request->setPage($page);
+
+        list($response, $status) = $client->Pali($request)->wait();
+        if ($status->code !== \Grpc\STATUS_OK) {
+            Log::error("ERROR: " . $status->code . ", " . $status->details);
+            return false;
+        }
+        $output = [];
+        $output['total'] = $response->getTotal();
+        $output['rows'] = [];
+        foreach ($response->getItems() as $key => $value) {
+            $output['rows'][] = (object)[
+                'rank' => $value->getRank(),
+                'highlight' => $value->getHighlight(),
+                'book' => $value->getBook(),
+                'paragraph' => $value->getParagraph(),
+                'content' => $value->getContent(),
+            ];
+        }
+        return $output;
+    }
+
+    public static function book_list($words,$books,$matchMode='case',$index=0,$size=10){
+        $host = config('mint.server.rpc.tulip');
+        Log::debug('tulip host='.$host);
+        $client = new \Mint\Tulip\V1\SearchClient($host, [
+            'credentials' => \Grpc\ChannelCredentials::createInsecure(),
+        ]);
+
+        $request = new \Mint\Tulip\V1\SearchRequest();
+        $request->setKeywords($words);
+        $request->setBooks($books);
+        $request->setMatchMode($matchMode);
+        $page = new \Mint\Tulip\V1\SearchRequest\Page;
+        $page->setIndex($index);
+        $page->setSize($size);
+        $request->setPage($page);
+
+        list($response, $status) = $client->BookList($request)->wait();
+        if ($status->code !== \Grpc\STATUS_OK) {
+            Log::error("ERROR: " . $status->code . ", " . $status->details);
+            return false;
+        }
+        $output = [];
+        $output['rows'] = [];
+        foreach ($response->getItems() as $key => $value) {
+            $output['rows'][] = (object)[
+                'pcd_book_id' => $value->getBook(),
+                'co' => $value->getCount(),
+            ];
+        }
+        return $output;
+    }
+
+    public static function upload_dict($data){
+        $host = config('mint.server.rpc.tulip');
+        Log::debug('tulip host='.$host);
+        $client = new \Mint\Tulip\V1\SearchClient($host, [
+            'credentials' => \Grpc\ChannelCredentials::createInsecure(),
+        ]);
+
+        $request = new \Mint\Tulip\V1\UploadDictionaryRequest();
+        $request->setData($data);
+
+        list($response, $status) = $client->UploadDictionary($request)->wait();
+        if ($status->code !== \Grpc\STATUS_OK) {
+            Log::error("ERROR: " . $status->code . ", " . $status->details);
+            return false;
+        }
+        return $response->getError();
+    }
+
+    public static function update($book,$paragraph,
+                                  $bold1,$bold2,$bold3,
+                                  $content,$pcd_book_id){
+        $host = config('mint.server.rpc.tulip');
+        Log::debug('tulip host='.$host);
+        $client = new \Mint\Tulip\V1\SearchClient($host, [
+            'credentials' => \Grpc\ChannelCredentials::createInsecure(),
+        ]);
+
+        $request = new \Mint\Tulip\V1\UpdateRequest();
+        $request->setBook($book);
+        $request->setParagraph($paragraph);
+        $request->setLevel(0);
+        $request->setBold1($bold1);
+        $request->setBold2($bold2);
+        $request->setBold3($bold3);
+        $request->setContent($content);
+        $request->setPcdBookId($pcd_book_id);
+
+        list($response, $status) = $client->Update($request)->wait();
+        if ($status->code !== \Grpc\STATUS_OK) {
+            Log::error("ERROR: " . $status->code . ", " . $status->details);
+            return false;
+        }
+        return $response->getCount();
+    }
+}

+ 2 - 0
config/mint.php

@@ -19,6 +19,8 @@ return [
             'morus' => env('MORUS_RPC_SERVER', "localhost:9999"),
 
             'lily' => env('LILY_RPC_SERVER', "192.168.43.100:9000"),
+
+            'tulip' => env('TULIP_RPC_SERVER', "localhost:9990"),
         ],
 
         'assets' => env('ASSETS_SERVER', "localhost:9999"),

BIN
grpc/GPBMetadata/Tulip.php


+ 58 - 0
grpc/Mint/Tulip/V1/BookListResponse.php

@@ -0,0 +1,58 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.BookListResponse</code>
+ */
+class BookListResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.BookListResponse.Item items = 1;</code>
+     */
+    private $items;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type array<\Mint\Tulip\V1\BookListResponse\Item>|\Google\Protobuf\Internal\RepeatedField $items
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.BookListResponse.Item items = 1;</code>
+     * @return \Google\Protobuf\Internal\RepeatedField
+     */
+    public function getItems()
+    {
+        return $this->items;
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.BookListResponse.Item items = 1;</code>
+     * @param array<\Mint\Tulip\V1\BookListResponse\Item>|\Google\Protobuf\Internal\RepeatedField $var
+     * @return $this
+     */
+    public function setItems($var)
+    {
+        $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Mint\Tulip\V1\BookListResponse\Item::class);
+        $this->items = $arr;
+
+        return $this;
+    }
+
+}
+

+ 88 - 0
grpc/Mint/Tulip/V1/BookListResponse/Item.php

@@ -0,0 +1,88 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1\BookListResponse;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.BookListResponse.Item</code>
+ */
+class Item extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     */
+    protected $book = 0;
+    /**
+     * Generated from protobuf field <code>int32 count = 2;</code>
+     */
+    protected $count = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $book
+     *     @type int $count
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @return int
+     */
+    public function getBook()
+    {
+        return $this->book;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setBook($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->book = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 count = 2;</code>
+     * @return int
+     */
+    public function getCount()
+    {
+        return $this->count;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 count = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setCount($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->count = $var;
+
+        return $this;
+    }
+
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Item::class, \Mint\Tulip\V1\BookListResponse_Item::class);
+

+ 89 - 0
grpc/Mint/Tulip/V1/SearchClient.php

@@ -0,0 +1,89 @@
+<?php
+// GENERATED CODE -- DO NOT EDIT!
+
+namespace Mint\Tulip\V1;
+
+/**
+ */
+class SearchClient extends \Grpc\BaseStub {
+
+    /**
+     * @param string $hostname hostname
+     * @param array $opts channel options
+     * @param \Grpc\Channel $channel (optional) re-use channel object
+     */
+    public function __construct($hostname, $opts, $channel = null) {
+        parent::__construct($hostname, $opts, $channel);
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\SearchRequest $argument input argument
+     * @param array $metadata metadata
+     * @param array $options call options
+     * @return \Grpc\UnaryCall
+     */
+    public function Pali(\Mint\Tulip\V1\SearchRequest $argument,
+      $metadata = [], $options = []) {
+        return $this->_simpleRequest('/mint.tulip.v1.Search/Pali',
+        $argument,
+        ['\Mint\Tulip\V1\SearchResponse', 'decode'],
+        $metadata, $options);
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\SearchRequest $argument input argument
+     * @param array $metadata metadata
+     * @param array $options call options
+     * @return \Grpc\UnaryCall
+     */
+    public function BookList(\Mint\Tulip\V1\SearchRequest $argument,
+      $metadata = [], $options = []) {
+        return $this->_simpleRequest('/mint.tulip.v1.Search/BookList',
+        $argument,
+        ['\Mint\Tulip\V1\BookListResponse', 'decode'],
+        $metadata, $options);
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UpdateRequest $argument input argument
+     * @param array $metadata metadata
+     * @param array $options call options
+     * @return \Grpc\UnaryCall
+     */
+    public function Update(\Mint\Tulip\V1\UpdateRequest $argument,
+      $metadata = [], $options = []) {
+        return $this->_simpleRequest('/mint.tulip.v1.Search/Update',
+        $argument,
+        ['\Mint\Tulip\V1\UpdateResponse', 'decode'],
+        $metadata, $options);
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UpdateIndexRequest $argument input argument
+     * @param array $metadata metadata
+     * @param array $options call options
+     * @return \Grpc\UnaryCall
+     */
+    public function UpdateIndex(\Mint\Tulip\V1\UpdateIndexRequest $argument,
+      $metadata = [], $options = []) {
+        return $this->_simpleRequest('/mint.tulip.v1.Search/UpdateIndex',
+        $argument,
+        ['\Mint\Tulip\V1\UpdateIndexResponse', 'decode'],
+        $metadata, $options);
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UploadDictionaryRequest $argument input argument
+     * @param array $metadata metadata
+     * @param array $options call options
+     * @return \Grpc\UnaryCall
+     */
+    public function UploadDictionary(\Mint\Tulip\V1\UploadDictionaryRequest $argument,
+      $metadata = [], $options = []) {
+        return $this->_simpleRequest('/mint.tulip.v1.Search/UploadDictionary',
+        $argument,
+        ['\Mint\Tulip\V1\UploadDictionaryResponse', 'decode'],
+        $metadata, $options);
+    }
+
+}

+ 151 - 0
grpc/Mint/Tulip/V1/SearchRequest.php

@@ -0,0 +1,151 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * ----------------------------------------------------------------------------
+ *
+ * Generated from protobuf message <code>mint.tulip.v1.SearchRequest</code>
+ */
+class SearchRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>repeated string keywords = 1;</code>
+     */
+    private $keywords;
+    /**
+     * Generated from protobuf field <code>repeated int32 books = 2;</code>
+     */
+    private $books;
+    /**
+     * Generated from protobuf field <code>string match_mode = 3;</code>
+     */
+    protected $match_mode = '';
+    /**
+     * Generated from protobuf field <code>optional .mint.tulip.v1.SearchRequest.Page page = 99;</code>
+     */
+    protected $page = null;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type array<string>|\Google\Protobuf\Internal\RepeatedField $keywords
+     *     @type array<int>|\Google\Protobuf\Internal\RepeatedField $books
+     *     @type string $match_mode
+     *     @type \Mint\Tulip\V1\SearchRequest\Page $page
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated string keywords = 1;</code>
+     * @return \Google\Protobuf\Internal\RepeatedField
+     */
+    public function getKeywords()
+    {
+        return $this->keywords;
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated string keywords = 1;</code>
+     * @param array<string>|\Google\Protobuf\Internal\RepeatedField $var
+     * @return $this
+     */
+    public function setKeywords($var)
+    {
+        $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::STRING);
+        $this->keywords = $arr;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated int32 books = 2;</code>
+     * @return \Google\Protobuf\Internal\RepeatedField
+     */
+    public function getBooks()
+    {
+        return $this->books;
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated int32 books = 2;</code>
+     * @param array<int>|\Google\Protobuf\Internal\RepeatedField $var
+     * @return $this
+     */
+    public function setBooks($var)
+    {
+        $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::INT32);
+        $this->books = $arr;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string match_mode = 3;</code>
+     * @return string
+     */
+    public function getMatchMode()
+    {
+        return $this->match_mode;
+    }
+
+    /**
+     * Generated from protobuf field <code>string match_mode = 3;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setMatchMode($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->match_mode = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>optional .mint.tulip.v1.SearchRequest.Page page = 99;</code>
+     * @return \Mint\Tulip\V1\SearchRequest\Page|null
+     */
+    public function getPage()
+    {
+        return $this->page;
+    }
+
+    public function hasPage()
+    {
+        return isset($this->page);
+    }
+
+    public function clearPage()
+    {
+        unset($this->page);
+    }
+
+    /**
+     * Generated from protobuf field <code>optional .mint.tulip.v1.SearchRequest.Page page = 99;</code>
+     * @param \Mint\Tulip\V1\SearchRequest\Page $var
+     * @return $this
+     */
+    public function setPage($var)
+    {
+        GPBUtil::checkMessage($var, \Mint\Tulip\V1\SearchRequest\Page::class);
+        $this->page = $var;
+
+        return $this;
+    }
+
+}
+

+ 88 - 0
grpc/Mint/Tulip/V1/SearchRequest/Page.php

@@ -0,0 +1,88 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1\SearchRequest;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.SearchRequest.Page</code>
+ */
+class Page extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 index = 1;</code>
+     */
+    protected $index = 0;
+    /**
+     * Generated from protobuf field <code>int32 size = 2;</code>
+     */
+    protected $size = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $index
+     *     @type int $size
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 index = 1;</code>
+     * @return int
+     */
+    public function getIndex()
+    {
+        return $this->index;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 index = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setIndex($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->index = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 size = 2;</code>
+     * @return int
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 size = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setSize($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->size = $var;
+
+        return $this;
+    }
+
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Page::class, \Mint\Tulip\V1\SearchRequest_Page::class);
+

+ 122 - 0
grpc/Mint/Tulip/V1/SearchResponse.php

@@ -0,0 +1,122 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.SearchResponse</code>
+ */
+class SearchResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.SearchResponse.Item items = 1;</code>
+     */
+    private $items;
+    /**
+     * Generated from protobuf field <code>.mint.tulip.v1.SearchRequest.Page page = 98;</code>
+     */
+    protected $page = null;
+    /**
+     * Generated from protobuf field <code>int32 total = 99;</code>
+     */
+    protected $total = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type array<\Mint\Tulip\V1\SearchResponse\Item>|\Google\Protobuf\Internal\RepeatedField $items
+     *     @type \Mint\Tulip\V1\SearchRequest\Page $page
+     *     @type int $total
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.SearchResponse.Item items = 1;</code>
+     * @return \Google\Protobuf\Internal\RepeatedField
+     */
+    public function getItems()
+    {
+        return $this->items;
+    }
+
+    /**
+     * Generated from protobuf field <code>repeated .mint.tulip.v1.SearchResponse.Item items = 1;</code>
+     * @param array<\Mint\Tulip\V1\SearchResponse\Item>|\Google\Protobuf\Internal\RepeatedField $var
+     * @return $this
+     */
+    public function setItems($var)
+    {
+        $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Mint\Tulip\V1\SearchResponse\Item::class);
+        $this->items = $arr;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>.mint.tulip.v1.SearchRequest.Page page = 98;</code>
+     * @return \Mint\Tulip\V1\SearchRequest\Page|null
+     */
+    public function getPage()
+    {
+        return $this->page;
+    }
+
+    public function hasPage()
+    {
+        return isset($this->page);
+    }
+
+    public function clearPage()
+    {
+        unset($this->page);
+    }
+
+    /**
+     * Generated from protobuf field <code>.mint.tulip.v1.SearchRequest.Page page = 98;</code>
+     * @param \Mint\Tulip\V1\SearchRequest\Page $var
+     * @return $this
+     */
+    public function setPage($var)
+    {
+        GPBUtil::checkMessage($var, \Mint\Tulip\V1\SearchRequest\Page::class);
+        $this->page = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 total = 99;</code>
+     * @return int
+     */
+    public function getTotal()
+    {
+        return $this->total;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 total = 99;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setTotal($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->total = $var;
+
+        return $this;
+    }
+
+}
+

+ 169 - 0
grpc/Mint/Tulip/V1/SearchResponse/Item.php

@@ -0,0 +1,169 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1\SearchResponse;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.SearchResponse.Item</code>
+ */
+class Item extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 rank = 1;</code>
+     */
+    protected $rank = 0;
+    /**
+     * Generated from protobuf field <code>string highlight = 2;</code>
+     */
+    protected $highlight = '';
+    /**
+     * Generated from protobuf field <code>int32 book = 3;</code>
+     */
+    protected $book = 0;
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 4;</code>
+     */
+    protected $paragraph = 0;
+    /**
+     * Generated from protobuf field <code>string content = 5;</code>
+     */
+    protected $content = '';
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $rank
+     *     @type string $highlight
+     *     @type int $book
+     *     @type int $paragraph
+     *     @type string $content
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 rank = 1;</code>
+     * @return int
+     */
+    public function getRank()
+    {
+        return $this->rank;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 rank = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setRank($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->rank = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string highlight = 2;</code>
+     * @return string
+     */
+    public function getHighlight()
+    {
+        return $this->highlight;
+    }
+
+    /**
+     * Generated from protobuf field <code>string highlight = 2;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setHighlight($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->highlight = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 3;</code>
+     * @return int
+     */
+    public function getBook()
+    {
+        return $this->book;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 3;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setBook($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->book = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 4;</code>
+     * @return int
+     */
+    public function getParagraph()
+    {
+        return $this->paragraph;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 4;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setParagraph($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->paragraph = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string content = 5;</code>
+     * @return string
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Generated from protobuf field <code>string content = 5;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setContent($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->content = $var;
+
+        return $this;
+    }
+
+}
+
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Item::class, \Mint\Tulip\V1\SearchResponse_Item::class);
+

+ 121 - 0
grpc/Mint/Tulip/V1/SearchStub.php

@@ -0,0 +1,121 @@
+<?php
+// GENERATED CODE -- DO NOT EDIT!
+
+namespace Mint\Tulip\V1;
+
+/**
+ */
+class SearchStub {
+
+    /**
+     * @param \Mint\Tulip\V1\SearchRequest $request client request
+     * @param \Grpc\ServerContext $context server request context
+     * @return \Mint\Tulip\V1\SearchResponse for response data, null if if error occured
+     *     initial metadata (if any) and status (if not ok) should be set to $context
+     */
+    public function Pali(
+        \Mint\Tulip\V1\SearchRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\SearchResponse {
+        $context->setStatus(\Grpc\Status::unimplemented());
+        return null;
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\SearchRequest $request client request
+     * @param \Grpc\ServerContext $context server request context
+     * @return \Mint\Tulip\V1\BookListResponse for response data, null if if error occured
+     *     initial metadata (if any) and status (if not ok) should be set to $context
+     */
+    public function BookList(
+        \Mint\Tulip\V1\SearchRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\BookListResponse {
+        $context->setStatus(\Grpc\Status::unimplemented());
+        return null;
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UpdateRequest $request client request
+     * @param \Grpc\ServerContext $context server request context
+     * @return \Mint\Tulip\V1\UpdateResponse for response data, null if if error occured
+     *     initial metadata (if any) and status (if not ok) should be set to $context
+     */
+    public function Update(
+        \Mint\Tulip\V1\UpdateRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\UpdateResponse {
+        $context->setStatus(\Grpc\Status::unimplemented());
+        return null;
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UpdateIndexRequest $request client request
+     * @param \Grpc\ServerContext $context server request context
+     * @return \Mint\Tulip\V1\UpdateIndexResponse for response data, null if if error occured
+     *     initial metadata (if any) and status (if not ok) should be set to $context
+     */
+    public function UpdateIndex(
+        \Mint\Tulip\V1\UpdateIndexRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\UpdateIndexResponse {
+        $context->setStatus(\Grpc\Status::unimplemented());
+        return null;
+    }
+
+    /**
+     * @param \Mint\Tulip\V1\UploadDictionaryRequest $request client request
+     * @param \Grpc\ServerContext $context server request context
+     * @return \Mint\Tulip\V1\UploadDictionaryResponse for response data, null if if error occured
+     *     initial metadata (if any) and status (if not ok) should be set to $context
+     */
+    public function UploadDictionary(
+        \Mint\Tulip\V1\UploadDictionaryRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\UploadDictionaryResponse {
+        $context->setStatus(\Grpc\Status::unimplemented());
+        return null;
+    }
+
+    /**
+     * Get the method descriptors of the service for server registration
+     *
+     * @return array of \Grpc\MethodDescriptor for the service methods
+     */
+    public final function getMethodDescriptors(): array
+    {
+        return [
+            '/mint.tulip.v1.Search/Pali' => new \Grpc\MethodDescriptor(
+                $this,
+                'Pali',
+                '\Mint\Tulip\V1\SearchRequest',
+                \Grpc\MethodDescriptor::UNARY_CALL
+            ),
+            '/mint.tulip.v1.Search/BookList' => new \Grpc\MethodDescriptor(
+                $this,
+                'BookList',
+                '\Mint\Tulip\V1\SearchRequest',
+                \Grpc\MethodDescriptor::UNARY_CALL
+            ),
+            '/mint.tulip.v1.Search/Update' => new \Grpc\MethodDescriptor(
+                $this,
+                'Update',
+                '\Mint\Tulip\V1\UpdateRequest',
+                \Grpc\MethodDescriptor::UNARY_CALL
+            ),
+            '/mint.tulip.v1.Search/UpdateIndex' => new \Grpc\MethodDescriptor(
+                $this,
+                'UpdateIndex',
+                '\Mint\Tulip\V1\UpdateIndexRequest',
+                \Grpc\MethodDescriptor::UNARY_CALL
+            ),
+            '/mint.tulip.v1.Search/UploadDictionary' => new \Grpc\MethodDescriptor(
+                $this,
+                'UploadDictionary',
+                '\Mint\Tulip\V1\UploadDictionaryRequest',
+                \Grpc\MethodDescriptor::UNARY_CALL
+            ),
+        ];
+    }
+
+}

+ 95 - 0
grpc/Mint/Tulip/V1/UpdateIndexRequest.php

@@ -0,0 +1,95 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UpdateIndexRequest</code>
+ */
+class UpdateIndexRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     */
+    protected $book = 0;
+    /**
+     * Generated from protobuf field <code>optional int32 paragraph = 2;</code>
+     */
+    protected $paragraph = null;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $book
+     *     @type int $paragraph
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @return int
+     */
+    public function getBook()
+    {
+        return $this->book;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setBook($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->book = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>optional int32 paragraph = 2;</code>
+     * @return int
+     */
+    public function getParagraph()
+    {
+        return isset($this->paragraph) ? $this->paragraph : 0;
+    }
+
+    public function hasParagraph()
+    {
+        return isset($this->paragraph);
+    }
+
+    public function clearParagraph()
+    {
+        unset($this->paragraph);
+    }
+
+    /**
+     * Generated from protobuf field <code>optional int32 paragraph = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setParagraph($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->paragraph = $var;
+
+        return $this;
+    }
+
+}
+

+ 58 - 0
grpc/Mint/Tulip/V1/UpdateIndexResponse.php

@@ -0,0 +1,58 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UpdateIndexResponse</code>
+ */
+class UpdateIndexResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     */
+    protected $error = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $error
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     * @return int
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setError($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->error = $var;
+
+        return $this;
+    }
+
+}
+

+ 247 - 0
grpc/Mint/Tulip/V1/UpdateRequest.php

@@ -0,0 +1,247 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UpdateRequest</code>
+ */
+class UpdateRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     */
+    protected $book = 0;
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 2;</code>
+     */
+    protected $paragraph = 0;
+    /**
+     * Generated from protobuf field <code>int32 level = 3;</code>
+     */
+    protected $level = 0;
+    /**
+     * Generated from protobuf field <code>string bold1 = 4;</code>
+     */
+    protected $bold1 = '';
+    /**
+     * Generated from protobuf field <code>string bold2 = 5;</code>
+     */
+    protected $bold2 = '';
+    /**
+     * Generated from protobuf field <code>string bold3 = 6;</code>
+     */
+    protected $bold3 = '';
+    /**
+     * Generated from protobuf field <code>string content = 7;</code>
+     */
+    protected $content = '';
+    /**
+     * Generated from protobuf field <code>int32 pcd_book_id = 8;</code>
+     */
+    protected $pcd_book_id = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $book
+     *     @type int $paragraph
+     *     @type int $level
+     *     @type string $bold1
+     *     @type string $bold2
+     *     @type string $bold3
+     *     @type string $content
+     *     @type int $pcd_book_id
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @return int
+     */
+    public function getBook()
+    {
+        return $this->book;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 book = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setBook($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->book = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 2;</code>
+     * @return int
+     */
+    public function getParagraph()
+    {
+        return $this->paragraph;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 paragraph = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setParagraph($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->paragraph = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 level = 3;</code>
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 level = 3;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setLevel($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->level = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold1 = 4;</code>
+     * @return string
+     */
+    public function getBold1()
+    {
+        return $this->bold1;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold1 = 4;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setBold1($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->bold1 = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold2 = 5;</code>
+     * @return string
+     */
+    public function getBold2()
+    {
+        return $this->bold2;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold2 = 5;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setBold2($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->bold2 = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold3 = 6;</code>
+     * @return string
+     */
+    public function getBold3()
+    {
+        return $this->bold3;
+    }
+
+    /**
+     * Generated from protobuf field <code>string bold3 = 6;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setBold3($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->bold3 = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string content = 7;</code>
+     * @return string
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Generated from protobuf field <code>string content = 7;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setContent($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->content = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 pcd_book_id = 8;</code>
+     * @return int
+     */
+    public function getPcdBookId()
+    {
+        return $this->pcd_book_id;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 pcd_book_id = 8;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setPcdBookId($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->pcd_book_id = $var;
+
+        return $this;
+    }
+
+}
+

+ 58 - 0
grpc/Mint/Tulip/V1/UpdateResponse.php

@@ -0,0 +1,58 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UpdateResponse</code>
+ */
+class UpdateResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 count = 1;</code>
+     */
+    protected $count = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $count
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 count = 1;</code>
+     * @return int
+     */
+    public function getCount()
+    {
+        return $this->count;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 count = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setCount($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->count = $var;
+
+        return $this;
+    }
+
+}
+

+ 58 - 0
grpc/Mint/Tulip/V1/UploadDictionaryRequest.php

@@ -0,0 +1,58 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UploadDictionaryRequest</code>
+ */
+class UploadDictionaryRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>string data = 1;</code>
+     */
+    protected $data = '';
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type string $data
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>string data = 1;</code>
+     * @return string
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Generated from protobuf field <code>string data = 1;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setData($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->data = $var;
+
+        return $this;
+    }
+
+}
+

+ 58 - 0
grpc/Mint/Tulip/V1/UploadDictionaryResponse.php

@@ -0,0 +1,58 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: tulip.proto
+
+namespace Mint\Tulip\V1;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>mint.tulip.v1.UploadDictionaryResponse</code>
+ */
+class UploadDictionaryResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     */
+    protected $error = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $error
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\Tulip::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     * @return int
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * Generated from protobuf field <code>int32 error = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setError($var)
+    {
+        GPBUtil::checkInt32($var);
+        $this->error = $var;
+
+        return $this;
+    }
+
+}
+

+ 1 - 1
public/pali_title/pali_serieses.csv

@@ -279,4 +279,4 @@
 278,216,3,(vn)cūḷavaggapāḷi
 279,217,3,(vn)parivārapāḷi
 280,139,925,samantapāsādikā
-276,214,2162,(VN)Bhikkhunīvibhaṅgo
+281,214,2162,(VN)Bhikkhunīvibhaṅgo