Преглед изворни кода

Merge pull request #1681 from visuddhinanda/agile

加入巴利文搜索微服务支持
visuddhinanda пре 2 година
родитељ
комит
cf20ce57cd

+ 94 - 0
dashboard/src/assets/icon/index.tsx

@@ -492,6 +492,88 @@ const PasteOutLined = () => (
     ></path>
   </svg>
 );
+
+const EpubOutLined = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="6403"
+    width="1em"
+    height="1em"
+  >
+    <path
+      d="M518.144 29.696v100.352H236.544c-6.144 0-8.192 2.048-8.192 8.192v666.624h566.272V27.648c12.288 0 23.552 2.048 34.816 6.144 33.792 13.312 55.296 37.888 64.512 73.728 1.024 5.12 2.048 10.24 2.048 16.384v776.192c0 23.552-9.216 44.032-25.6 61.44-13.312 15.36-29.696 25.6-49.152 30.72-8.192 2.048-16.384 4.096-24.576 4.096-189.44 0-378.88 1.024-569.344 0-48.128 0-87.04-33.792-98.304-80.896-1.024-6.144-1.024-12.288-1.024-18.432V132.096c0-36.864 16.384-65.536 46.08-86.016 15.36-10.24 32.768-15.36 52.224-15.36h282.624c3.072-1.024 6.144-1.024 9.216-1.024z m-49.152 868.352c0 25.6 20.48 44.032 43.008 44.032 24.576 0 41.984-19.456 43.008-41.984 1.024-24.576-22.528-44.032-43.008-45.056-22.528-1.024-44.032 21.504-43.008 43.008z"
+      fill="#0079C1"
+      p-id="6404"
+    ></path>
+    <path
+      d="M741.376 29.696V368.64l-1.024 1.024c-35.84-48.128-71.68-96.256-107.52-145.408-19.456 21.504-38.912 43.008-59.392 64.512V29.696h167.936z"
+      fill="#FFD204"
+      p-id="6405"
+    ></path>
+    <path
+      d="M284.672 584.704v-77.824h456.704v77.824H284.672z m456.704 76.8v67.584H284.672v-67.584h456.704zM284.672 351.232h344.064v77.824H284.672v-77.824z m0-65.536v-78.848h233.472v78.848H284.672z"
+      fill="#BABABA"
+      p-id="6406"
+    ></path>
+  </svg>
+);
+
+const HtmlOutLined = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="6553"
+    width="1em"
+    height="1em"
+  >
+    <path
+      d="M89.088 59.392l62.464 803.84c1.024 12.288 9.216 22.528 20.48 25.6L502.784 993.28c6.144 2.048 12.288 2.048 18.432 0l330.752-104.448c11.264-4.096 19.456-14.336 20.48-25.6l62.464-803.84c1.024-17.408-12.288-31.744-29.696-31.744H118.784c-17.408 0-31.744 14.336-29.696 31.744z"
+      fill="#FC490B"
+      p-id="6554"
+    ></path>
+    <path
+      d="M774.144 309.248h-409.6l12.288 113.664h388.096l-25.6 325.632-227.328 71.68-227.328-71.68-13.312-169.984h118.784v82.944l124.928 33.792 123.904-33.792 10.24-132.096H267.264L241.664 204.8h540.672z"
+      fill="#FFFFFF"
+      p-id="6555"
+    ></path>
+  </svg>
+);
+
+const DocOutLined = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="6702"
+    width="1em"
+    height="1em"
+  >
+    <path
+      d="M950.272 843.776H527.36c-16.384 0-29.696-13.312-29.696-29.696V210.944c0-16.384 13.312-29.696 29.696-29.696h422.912c16.384 0 29.696 13.312 29.696 29.696v603.136c0 16.384-13.312 29.696-29.696 29.696z"
+      fill="#E8E8E8"
+      p-id="6703"
+    ></path>
+    <path
+      d="M829.44 361.472H527.36c-16.384 0-29.696-13.312-29.696-29.696s13.312-29.696 29.696-29.696H829.44c16.384 0 29.696 13.312 29.696 29.696 0 15.36-13.312 29.696-29.696 29.696z m0 120.832H527.36c-16.384 0-29.696-13.312-29.696-29.696s13.312-29.696 29.696-29.696H829.44c16.384 0 29.696 13.312 29.696 29.696s-13.312 29.696-29.696 29.696z m0 119.808H527.36c-16.384 0-29.696-13.312-29.696-29.696s13.312-29.696 29.696-29.696H829.44c16.384 0 29.696 13.312 29.696 29.696s-13.312 29.696-29.696 29.696z m0 120.832H527.36c-16.384 0-29.696-13.312-29.696-29.696s13.312-29.696 29.696-29.696H829.44c16.384 0 29.696 13.312 29.696 29.696s-13.312 29.696-29.696 29.696z"
+      fill="#B2B2B2"
+      p-id="6704"
+    ></path>
+    <path
+      d="M607.232 995.328l-563.2-107.52V135.168l563.2-107.52v967.68z"
+      fill="#0D47A1"
+      p-id="6705"
+    ></path>
+    <path
+      d="M447.488 696.32h-71.68l-47.104-236.544c-3.072-13.312-4.096-27.648-4.096-40.96h-1.024c-1.024 16.384-3.072 30.72-5.12 40.96L269.312 696.32H194.56l-74.752-368.64h70.656l39.936 245.76c2.048 10.24 3.072 24.576 4.096 41.984h1.024c0-13.312 3.072-27.648 6.144-43.008l51.2-244.736h68.608l47.104 247.808c2.048 9.216 3.072 22.528 4.096 39.936h1.024c1.024-13.312 2.048-26.624 4.096-40.96l39.936-245.76H522.24L447.488 696.32z"
+      fill="#FFFFFF"
+      p-id="6706"
+    ></path>
+  </svg>
+);
+
 export const DictIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={DictSvg} {...props} />
 );
@@ -589,3 +671,15 @@ export const TransferOutLinedIcon = (
 export const PasteOutLinedIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={PasteOutLined} {...props} />
 );
+
+export const EpubIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={EpubOutLined} {...props} />
+);
+
+export const HtmlIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={HtmlOutLined} {...props} />
+);
+
+export const DocIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={DocOutLined} {...props} />
+);

+ 1 - 0
dashboard/src/components/article/Article.tsx

@@ -225,6 +225,7 @@ const ArticleWidget = ({
         });
       }
 
+      console.log("url", url);
       if (type === "term") {
         get<ITermResponse>(url)
           .then((json) => {

+ 228 - 0
dashboard/src/components/export/ExportModal.tsx

@@ -0,0 +1,228 @@
+import {
+  Modal,
+  Progress,
+  Select,
+  Space,
+  Switch,
+  Typography,
+  message,
+} from "antd";
+import { useEffect, useRef, useState } from "react";
+import { get } from "../../request";
+import { ArticleType } from "../article/Article";
+import ExportSettingLayout from "./ExportSettingLayout";
+
+const { Text } = Typography;
+
+interface IExportResponse {
+  ok: boolean;
+  message?: string;
+  data: string;
+}
+
+interface IStatus {
+  progress: number;
+  message: string;
+}
+interface IExportStatusResponse {
+  ok: boolean;
+  message?: string;
+  data: {
+    url?: string;
+    status: IStatus;
+  };
+}
+interface IWidget {
+  type?: ArticleType;
+  articleId?: string;
+  book?: string | null;
+  para?: string | null;
+  channelId?: string | null;
+  anthologyId?: string | null;
+  open?: boolean;
+  onClose?: Function;
+}
+
+const ExportModalWidget = ({
+  type,
+  book,
+  para,
+  channelId,
+  articleId,
+  anthologyId,
+  open = false,
+  onClose,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(open);
+  const [filename, setFilename] = useState<string>();
+  const [url, setUrl] = useState<string>();
+  const [format, setFormat] = useState<string>("html");
+  const [exportStatus, setExportStatus] = useState<IStatus>();
+  const [exportStart, setExportStart] = useState(false);
+  const filenameRef = useRef(filename);
+
+  useEffect(() => {
+    // 及时更新 count 值
+    filenameRef.current = filename;
+  });
+  const queryStatus = () => {
+    console.log("timer", filenameRef.current);
+    if (typeof filenameRef.current === "undefined") {
+      return;
+    }
+    const url = `/v2/export/${filenameRef.current}`;
+    console.log("url", url);
+    get<IExportStatusResponse>(url).then((json) => {
+      if (json.ok) {
+        console.log("filename", json);
+        setExportStatus(json.data.status);
+        if (json.data.status.progress === 1) {
+          setFilename(undefined);
+          setUrl(json.data.url);
+        }
+      } else {
+      }
+    });
+  };
+
+  useEffect(() => {
+    const interval = setInterval(() => queryStatus(), 3000);
+    return () => clearInterval(interval);
+  }, []);
+
+  const exportChapter = (
+    book: number,
+    para: number,
+    channel: string,
+    format: string
+  ): void => {
+    const url = `/v2/export?book=${book}&par=${para}&channel=${channel}&format=${format}`;
+    console.log("url", url);
+    setExportStart(true);
+    get<IExportResponse>(url).then((json) => {
+      if (json.ok) {
+        const filename = json.data;
+        console.log("filename", filename);
+        setFilename(filename);
+      } else {
+      }
+    });
+  };
+  const closeModal = () => {
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
+  };
+  useEffect(() => setIsModalOpen(open), [open]);
+  return (
+    <Modal
+      destroyOnClose
+      title="导出"
+      width={400}
+      open={isModalOpen}
+      onOk={() => {
+        console.log("type", type);
+        if (type === "chapter") {
+          if (articleId && channelId) {
+            const para = articleId.split("-").map((item) => parseInt(item));
+            const channels = channelId.split("_");
+            if (para.length === 2) {
+              exportChapter(para[0], para[1], channels[0], "html");
+            } else {
+              console.error("段落编号错误", articleId);
+            }
+          }
+        } else {
+          message.error("目前只支持章节导出");
+        }
+      }}
+      onCancel={closeModal}
+      okText={"导出"}
+      okButtonProps={{ disabled: exportStart }}
+    >
+      <ExportSettingLayout
+        label="格式"
+        content={
+          <Select
+            defaultValue={format}
+            bordered={false}
+            options={[
+              {
+                value: "pdf",
+                label: "PDF",
+                disabled: true,
+              },
+              {
+                value: "word",
+                label: "Word",
+                disabled: true,
+              },
+              {
+                value: "html",
+                label: "Html",
+              },
+            ]}
+            onSelect={(value) => setFormat(value)}
+          />
+        }
+      />
+      <ExportSettingLayout
+        label="原文"
+        content={<Switch size="small" onChange={(checked) => {}} />}
+      />
+      <ExportSettingLayout
+        label="译文"
+        content={
+          <Switch size="small" defaultChecked onChange={(checked) => {}} />
+        }
+      />
+      <ExportSettingLayout
+        label="对照方式"
+        content={
+          <Select
+            defaultValue={"auto"}
+            bordered={false}
+            options={[
+              {
+                value: "auto",
+                label: "自动",
+              },
+              {
+                value: "col",
+                label: "分栏",
+              },
+              {
+                value: "row",
+                label: "纵列",
+              },
+            ]}
+            onSelect={(value) => setFormat(value)}
+          />
+        }
+      />
+
+      <div style={{ display: exportStart ? "block" : "none" }}>
+        <Text>{exportStatus ? exportStatus.message : "正在生成……"}</Text>
+        <Progress
+          percent={exportStatus ? Math.round(exportStatus?.progress * 100) : 0}
+          status={
+            exportStatus
+              ? exportStatus.progress === 1
+                ? "success"
+                : "active"
+              : "normal"
+          }
+        />
+        {url ? (
+          <a href={url} target="_blank" rel="noreferrer">
+            {"下载"}
+          </a>
+        ) : (
+          <></>
+        )}
+      </div>
+    </Modal>
+  );
+};
+
+export default ExportModalWidget;

+ 20 - 0
dashboard/src/components/export/ExportSettingLayout.tsx

@@ -0,0 +1,20 @@
+interface IWidget {
+  label?: string;
+  content?: React.ReactNode;
+}
+const ExportSettingLayoutWidget = ({ label, content }: IWidget) => {
+  return (
+    <div
+      style={{
+        display: "flex",
+        justifyContent: "space-between",
+        marginBottom: 4,
+      }}
+    >
+      <span>{label}</span>
+      <span>{content}</span>
+    </div>
+  );
+};
+
+export default ExportSettingLayoutWidget;

+ 74 - 0
dashboard/src/components/export/ShareButton.tsx

@@ -0,0 +1,74 @@
+import { Button, Dropdown, Space, Typography } from "antd";
+import { ShareAltOutlined, ExportOutlined } from "@ant-design/icons";
+import ExportModal from "./ExportModal";
+import { useState } from "react";
+import { ArticleType } from "../article/Article";
+
+const { Text } = Typography;
+
+interface IWidget {
+  type?: ArticleType;
+  articleId?: string;
+  book?: string | null;
+  para?: string | null;
+  channelId?: string | null;
+  anthologyId?: string | null;
+}
+const ShareButtonWidget = ({
+  type,
+  book,
+  para,
+  channelId,
+  articleId,
+  anthologyId,
+}: IWidget) => {
+  const [exportOpen, setExportOpen] = useState(false);
+
+  return (
+    <>
+      <Dropdown
+        trigger={["click"]}
+        menu={{
+          items: [
+            {
+              label: (
+                <Space>
+                  {"Export"}
+                  <Text type="secondary" style={{ fontSize: "80%" }}>
+                    {"PDF,Word,Html"}
+                  </Text>
+                </Space>
+              ),
+              key: "export",
+              icon: <ExportOutlined />,
+            },
+          ],
+          onClick: ({ key }) => {
+            switch (key) {
+              case "export":
+                setExportOpen(true);
+                break;
+
+              default:
+                break;
+            }
+          },
+        }}
+      >
+        <Button type="text" icon={<ShareAltOutlined color="#fff" />} />
+      </Dropdown>
+      <ExportModal
+        type={type}
+        articleId={articleId}
+        book={book}
+        para={para}
+        channelId={channelId}
+        anthologyId={anthologyId}
+        open={exportOpen}
+        onClose={() => setExportOpen(false)}
+      />
+    </>
+  );
+};
+
+export default ShareButtonWidget;

+ 13 - 7
dashboard/src/components/template/utilities.ts

@@ -93,13 +93,19 @@ export function XmlToReact(
               );
               break;
             default:
-              output.push(
-                React.createElement(
-                  tagName,
-                  getAttr(value, i),
-                  convert(value, wordWidget, convertor)
-                )
-              );
+              try {
+                output.push(
+                  React.createElement(
+                    tagName,
+                    getAttr(value, i),
+                    convert(value, wordWidget, convertor)
+                  )
+                );
+              } catch (error) {
+                console.log("ParserError", tagName);
+                output.push(React.createElement(ParserError, { key: i }, []));
+              }
+
               break;
           }
 

+ 12 - 3
dashboard/src/pages/library/article/show.tsx

@@ -50,6 +50,7 @@ import SearchButton from "../../../components/general/SearchButton";
 import ToStudio from "../../../components/auth/ToStudio";
 import { currentUser as _currentUser } from "../../../reducers/current-user";
 import LoginAlertModal from "../../../components/auth/LoginAlertModal";
+import ShareButton from "../../../components/export/ShareButton";
 
 /**
  * type:
@@ -63,11 +64,11 @@ import LoginAlertModal from "../../../components/auth/LoginAlertModal";
  * @returns
  */
 const Widget = () => {
-  const { type, id, mode = "read" } = useParams(); //url 参数
+  const { type, id } = useParams(); //url 参数
+  const [searchParams, setSearchParams] = useSearchParams();
+
   const navigate = useNavigate();
-  console.log("mode", mode);
   const [rightPanel, setRightPanel] = useState<TPanelName>("close");
-  const [searchParams, setSearchParams] = useSearchParams();
   const [anchorNavOpen, setAnchorNavOpen] = useState(false);
   const [anchorNavShow, setAnchorNavShow] = useState(true);
   const [recentModalOpen, setRecentModalOpen] = useState(false);
@@ -187,6 +188,14 @@ const Widget = () => {
                 </Button>
               </>
             ) : undefined}
+            <ShareButton
+              type={type as ArticleType}
+              book={searchParams.get("book")}
+              para={searchParams.get("par")}
+              channelId={searchParams.get("channel")}
+              articleId={id}
+              anthologyId={searchParams.get("anthology")}
+            />
             <SearchButton />
             <Divider type="vertical" />
             <ToStudio />

+ 50 - 1
rpc/protocols/tulip.proto

@@ -6,7 +6,8 @@ package mint.tulip.v1;
 // ----------------------------------------------------------------------------
 message SearchRequest {
   repeated string keywords = 1;
-  int32 book = 2;
+  repeated int32 books = 2;
+  string match_mode = 3;
 
   message Page {
     int32 index = 1;
@@ -29,8 +30,56 @@ message SearchResponse {
   int32 total = 99;
 }
 
+message BookListResponse {
+  message Item {
+    int32 book = 1;
+    int32 count = 2;
+  }
+  repeated Item items = 1;
+}
+
+message UpdateRequest {
+  int32 book = 1;
+  int32 paragraph = 2;
+  int32 level = 3;
+  string bold1 = 4;
+  string bold2 = 5;
+  string bold3 = 6;
+  string content = 7;
+  int32 pcd_book_id = 8;
+}
+message UpdateResponse{
+  int32 count = 1;
+}
+
+message UpdateIndexRequest{
+  int32 book = 1;
+  optional int32 paragraph = 2;
+}
+
+message UpdateIndexResponse{
+  int32 error = 1;
+}
+
+message UploadDictionaryRequest{
+  string data = 1;
+}
+
+message UploadDictionaryResponse{
+  int32 error = 1;
+}
+
 service Search {
   rpc Pali(SearchRequest) returns (SearchResponse) {}
+
+  rpc BookList(SearchRequest) returns (BookListResponse) {}
+
+  rpc Update(UpdateRequest) returns (UpdateResponse) {}
+
+  rpc UpdateIndex(UpdateIndexRequest) returns (UpdateIndexResponse) {}
+
+  rpc UploadDictionary(UploadDictionaryRequest) returns (UploadDictionaryResponse) {}
+
 }
 
 // ----------------------------------------------------------------------------

+ 2 - 0
rpc/tulip/tulip/.gitignore

@@ -1,2 +1,4 @@
 /vendor/
 /composer.lock
+/config.php
+/storage/

BIN
rpc/tulip/tulip/GPBMetadata/Tulip.php


+ 58 - 0
rpc/tulip/tulip/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
rpc/tulip/tulip/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);
+

+ 56 - 0
rpc/tulip/tulip/Mint/Tulip/V1/SearchClient.php

@@ -30,4 +30,60 @@ class SearchClient extends \Grpc\BaseStub {
         $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);
+    }
+
 }

+ 39 - 12
rpc/tulip/tulip/Mint/Tulip/V1/SearchRequest.php

@@ -20,9 +20,13 @@ class SearchRequest extends \Google\Protobuf\Internal\Message
      */
     private $keywords;
     /**
-     * Generated from protobuf field <code>int32 book = 2;</code>
+     * Generated from protobuf field <code>repeated int32 books = 2;</code>
      */
-    protected $book = 0;
+    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>
      */
@@ -35,7 +39,8 @@ class SearchRequest extends \Google\Protobuf\Internal\Message
      *     Optional. Data for populating the Message object.
      *
      *     @type array<string>|\Google\Protobuf\Internal\RepeatedField $keywords
-     *     @type int $book
+     *     @type array<int>|\Google\Protobuf\Internal\RepeatedField $books
+     *     @type string $match_mode
      *     @type \Mint\Tulip\V1\SearchRequest\Page $page
      * }
      */
@@ -67,23 +72,45 @@ class SearchRequest extends \Google\Protobuf\Internal\Message
     }
 
     /**
-     * Generated from protobuf field <code>int32 book = 2;</code>
-     * @return int
+     * 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 getBook()
+    public function getMatchMode()
     {
-        return $this->book;
+        return $this->match_mode;
     }
 
     /**
-     * Generated from protobuf field <code>int32 book = 2;</code>
-     * @param int $var
+     * Generated from protobuf field <code>string match_mode = 3;</code>
+     * @param string $var
      * @return $this
      */
-    public function setBook($var)
+    public function setMatchMode($var)
     {
-        GPBUtil::checkInt32($var);
-        $this->book = $var;
+        GPBUtil::checkString($var, True);
+        $this->match_mode = $var;
 
         return $this;
     }

+ 80 - 0
rpc/tulip/tulip/Mint/Tulip/V1/SearchStub.php

@@ -21,6 +21,62 @@ class SearchStub {
         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
      *
@@ -35,6 +91,30 @@ class SearchStub {
                 '\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
rpc/tulip/tulip/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
rpc/tulip/tulip/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
rpc/tulip/tulip/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
rpc/tulip/tulip/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
rpc/tulip/tulip/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
rpc/tulip/tulip/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;
+    }
+
+}
+

+ 13 - 0
rpc/tulip/tulip/config.sample.php

@@ -0,0 +1,13 @@
+<?php
+
+define("Config", [
+    'port' => 9990,
+    "database" => [
+        "driver" => "pgsql",
+        "host" => "localhost",
+        "port" => 5432,
+        "name" => "wikipali",
+        "user" => "postgres",
+        "password" => "123456",
+    ],
+]);

+ 387 - 9
rpc/tulip/tulip/server.php

@@ -1,29 +1,407 @@
 <?php
 
 require dirname(__FILE__) . '/vendor/autoload.php';
+require dirname(__FILE__) . '/config.php';
+
+
 
 class Greeter extends \Mint\Tulip\V1\SearchStub
 {
+    private $_pdo = null;
+    private function log($level,$message){
+        $output = "[\033[32m".date("Y/m/d h:i:sa") ."\033[0m] ";
+        if($level === 'error'){
+            $output .= "\033[41m" . $level . "\033[0m ";
+        }else{
+            $output .= $level;
+        }
+        
+        $output .= ' ' . $message.PHP_EOL;
+        if($level === 'error'){
+            fwrite(STDERR,$output);
+        }else{
+            fwrite(STDOUT,$output);
+        }
+    }
+    private function connectDb(){
+        /**
+         * 连接数据库
+         */
+        $db = Config['database']['driver'];
+        $db .= ":host=".Config['database']['host'];
+        $db .= ";port=".Config['database']['port'];
+        $db .= ";dbname=".Config['database']['name'];
+        $db .= ";user=".Config['database']['user'];
+        $db .= ";password=".Config['database']['password'].";";
+        echo 'connect to db host='.Config['database']['host'] . ' name='.Config['database']['name'].PHP_EOL;
+        try {
+            $PDO = new PDO($db,
+                        Config['database']['user'],
+                        Config['database']['password'],
+                        array(PDO::ATTR_PERSISTENT=>true));
+        }catch(PDOException $e) {
+            echo 'connect to db fail'.PHP_EOL;
+            print $e->getMessage();
+            return false;
+        }
+        $PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+        $this->_pdo = $PDO;
+    }
+    private function dbSelect($query, $params=null)
+    {
+        if($this->_pdo === null){
+            return false;
+        }
+        if (isset($params)) {
+            $stmt = $this->_pdo->prepare($query);
+            $stmt->execute($params);
+        } else {
+            $stmt = $this->_pdo->query($query);
+        }
+        return $stmt->fetchAll(PDO::FETCH_ASSOC);
+    }
+
+        /**
+     * Create a new instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->connectDb();
+    }
+
     public function Pali(
         \Mint\Tulip\V1\SearchRequest $request,
         \Grpc\ServerContext $context
     ): ?\Mint\Tulip\V1\SearchResponse {
-        $text = $request->getPayload();
-        echo 'Received request: ' . $text . PHP_EOL;
-        $response = new \Mint\Morus\V1\MarkdownToHtmlResponse();
-        $Parsedown = new Parsedown();
-        $response->setPayload($Parsedown->text($text));
+        $keyWords = [];
+        foreach ($request->getKeywords()->getIterator() as $word) {
+            $keyWords[] = $word;
+        }
+        echo "[".date("Y/m/d h:i:sa") ."] pali search: request words = ".implode(',',$keyWords) .PHP_EOL;
+
+        /**
+         * 查询业务逻辑
+         */
+
+        $searchChapters = [];
+        $searchBooks = [];
+        $searchBookId = [];
+        $bookId = [];
+        if($request->getBooks()->count()>0){
+            foreach ($request->getBooks()->getIterator() as $book) {
+                $bookId[] = $book;
+            }
+            $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
+        }else{
+            $queryBookId = '';
+        }
+        echo 'query books = '.implode(',',$bookId).PHP_EOL;
+
+        $matchMode = $request->getMatchMode();
+        echo 'query mode = '.$matchMode.PHP_EOL;
+        $param = [];
+        $countParam = [];
+        switch ($matchMode) {
+            case 'complete':
+            case 'case':
+                # code...
+                $querySelect_rank_base = " ts_rank('{0.1, 1, 0.3, 0.2}',
+                                                full_text_search_weighted,
+                                                websearch_to_tsquery('pali', ?)) ";
+                $querySelect_rank_head = implode('+', 
+                                            array_fill(0, count($keyWords), 
+                                            $querySelect_rank_base));
+
+                $param = array_merge($param,$keyWords);
+                $querySelect_rank = " {$querySelect_rank_head} AS rank, ";
+                $querySelect_highlight = " ts_headline('pali', content,
+                                            websearch_to_tsquery('pali', ?),
+                                            'StartSel = ~~, StopSel = ~~,MaxWords=3500, 
+                                            MinWords=3500,HighlightAll=TRUE')
+                                            AS highlight,";
+                array_push($param,implode(' ',$keyWords));
+                break;
+            case 'similar':
+                # 形似,去掉变音符号
+                $key = $this->getWordEn($keyWords[0]);
+                $querySelect_rank = "
+                    ts_rank('{0.1, 1, 0.3, 0.2}',
+                        full_text_search_weighted_unaccent,
+                        websearch_to_tsquery('pali_unaccent', ?))
+                    AS rank, ";
+                    $param[] = $key;
+                $querySelect_highlight = " ts_headline('pali_unaccent', content,
+                        websearch_to_tsquery('pali_unaccent', ?),
+                        'StartSel = ~~, StopSel = ~~,
+                        MaxWords=3500, MinWords=3500,
+                        HighlightAll=TRUE')
+                        AS highlight,";
+                $param[] = $key;
+                break;
+        }
+        $_queryWhere = $this->makeQueryWhere($keyWords,$matchMode);
+        $queryWhere = $_queryWhere['query'];
+        $param = array_merge($param,$_queryWhere['param']);
+
+        $querySelect_2 = "  book,paragraph,content ";
+
+        $queryCount = "SELECT count(*) as co FROM fts_texts WHERE {$queryWhere} {$queryBookId};";
+        $resultCount = $this->dbSelect($queryCount, $_queryWhere['param']);
+        $total = $resultCount[0]['co'];
+
+        if($request->hasPage()){
+            $limit = $request->getPage()->getSize();
+            $offset = $request->getPage()->getIndex();
+        }else{
+            $limit = 10;
+            $offset = 0;
+        }
+        $_orderBy = 'rank';
+        switch ($_orderBy) {
+            case 'rank':
+                $orderby = " ORDER BY rank DESC ";
+                break;
+            case 'paragraph':
+                $orderby = " ORDER BY book,paragraph ";
+                break;
+            default:
+                $orderby = "";
+                break;
+        };
+        $query = "SELECT
+            {$querySelect_rank}
+            {$querySelect_highlight}
+            {$querySelect_2}
+            FROM fts_texts
+            WHERE
+                {$queryWhere}
+                {$queryBookId}
+                {$orderby}
+            LIMIT ? OFFSET ? ;";
+        $param[] = $limit;
+        $param[] = $offset;
+
+        $result = $this->dbSelect($query, $param);
+
+         //返回数据
+        $response = new \Mint\Tulip\V1\SearchResponse();
+        $output = $response->getItems();
+        foreach ($result as $row) {
+            $item = new \Mint\Tulip\V1\SearchResponse\Item;
+            $item->setRank($row['rank']);
+            $item->setHighlight($row['highlight']);
+            $item->setBook($row['book']);
+            $item->setParagraph($row['paragraph']);
+            $item->setContent($row['content']);
+            $output[] = $item;
+        }
+        echo "total={$total}".PHP_EOL;
+        $response->setTotal($total);
+        return $response;
+    }
+
+    /**
+     * @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 {
+        $keyWords = [];
+        foreach ($request->getKeywords()->getIterator() as $word) {
+            $keyWords[] = $word;
+        }
+        echo "book list: request words = ".implode(',',$keyWords) .PHP_EOL;
+        /**
+         * 查询业务逻辑
+         */
+
+         $searchChapters = [];
+         $searchBooks = [];
+         $searchBookId = [];
+         $bookId = [];
+         if($request->getBooks()->count()>0){
+             foreach ($request->getBooks()->getIterator() as $book) {
+                 $bookId[] = $book;
+             }
+             $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
+         }else{
+             $queryBookId = '';
+         }
+         echo 'query books = '.implode(',',$bookId).PHP_EOL;
+ 
+         $matchMode = $request->getMatchMode();
+         echo 'query mode = '.$matchMode.PHP_EOL;
+         $queryWhere = $this->makeQueryWhere($keyWords,$matchMode);
+         $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 = $this->dbSelect($query, $queryWhere['param']);
+         //返回数据
+         $response = new \Mint\Tulip\V1\BookListResponse();
+         $output = $response->getItems();
+         foreach ($result as $row) {
+             $item = new \Mint\Tulip\V1\BookListResponse\Item;
+             $item->setBook($row['pcd_book_id']);
+             $item->setCount($row['co']);
+             $output[] = $item;
+         }
+         echo "total=".count($output).PHP_EOL;
+         return $response;
+    }
+    /**
+     * @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 {
+        $response = new \Mint\Tulip\V1\UploadDictionaryResponse();
+        $data = $request->getData();
+        $this->log('debug',"received data size=".strlen($data));
+        $dir = dirname(__FILE__) . '/storage';
+        if(!is_dir($dir)){
+            $res = mkdir($dir,0700,true);
+            if(!$res){
+                $this->log('error',"mkdir fail path=".$dir);
+                $response->setError(1);
+                return $response;
+            }
+        }
+        $filename = $dir.'/pali-'.date("Y-m-d-h-i-sa").'.syn';
+        $size = file_put_contents($filename,$data);
+
+        if($size === false){
+            $this->log('error',"file write fail ");
+            $response->setError(1);
+            return $response;
+        }
+        $this->log('debug',"save file size={$size} ");
+        $response->setError(0);
         return $response;
     }
+
+    public function Update(
+        \Mint\Tulip\V1\UpdateRequest $request,
+        \Grpc\ServerContext $context
+    ): ?\Mint\Tulip\V1\UpdateResponse {
+        $response = new \Mint\Tulip\V1\UpdateResponse();
+        $book = $request->getBook();
+        $paragraph = $request->getParagraph();
+        $this->log('debug',"update start book={$book} para={$paragraph} ");
+        $now = date("Y-m-d H:i:s");
+        //查询是否存在
+        $query = 'SELECT id from fts_texts where book=? and paragraph = ?';
+        $result = $this->dbSelect($query, [$book,$paragraph]);
+        if(count($result) >0 ){
+            //存在 update
+            $query = 'UPDATE fts_texts set 
+                                "bold_single"=?,
+                                "bold_double"=?,
+                                "bold_multiple"=?,
+                                "content"=?,
+                                "pcd_book_id"=?,
+                                "updated_at"=?  where id=? ';
+            $update = $this->dbSelect($query, [
+                                $request->getBold1(),
+                                $request->getBold2(),
+                                $request->getBold3(),
+                                $request->getContent(),
+                                $request->getPcdBookId(),
+                                $now,
+                                $result[0]['id']
+                                    ]);
+        }else{
+            // new
+            $query = "INSERT INTO fts_texts (
+                        book,
+                        paragraph,
+                        wid,
+                        bold_single,
+                        bold_double,
+                        bold_multiple,
+                        \"content\",
+                        created_at,
+                        updated_at,
+                        pcd_book_id) VALUES
+            (?,?,'bodytext',?,?,?,?,?,?,? )";
+            $insert = $this->dbSelect($query, [
+                            $request->getBook(),
+                            $request->getParagraph(),
+                            $request->getBold1(),
+                            $request->getBold2(),
+                            $request->getBold3(),
+                            $request->getContent(),
+                            $now,
+                            $now,
+                            $request->getPcdBookId(),
+                                ]);
+        }
+
+        $response->setCount(0);
+        return $response;
+    }
+
+    private function updateIndex($book,$para){
+        $query = 'UPDATE fts_texts SET content = content,
+        bold_single = bold_single,
+        bold_double = bold_double,
+        bold_multiple = bold_multiple
+        WHERE book = ? AND paragraph = ?';
+        $update = $this->dbSelect($query, [$book,$para]);
+    }
+    
+    private function updateIndexAll(){
+        $query = 'UPDATE fts_texts SET content = content,
+        bold_single = bold_single,
+        bold_double = bold_double,
+        bold_multiple = bold_multiple';
+        $update = $this->dbSelect($query);
+    }
+
+    private function makeQueryWhere($key,$match){
+        $param = [];
+        $queryWhere = '';
+        switch ($match) {
+            case 'complete':
+            case 'case':
+                # code...
+                $queryWhereBase = " full_text_search_weighted @@ websearch_to_tsquery('pali', ?) ";
+                $queryWhereBody = implode(' or ', array_fill(0, count($key), 
+                                    $queryWhereBase));
+                $queryWhere = " ({$queryWhereBody}) ";
+                $param = array_merge($param,$key);
+                break;
+            case 'similar':
+                # 形似,去掉变音符号
+                $queryWhere = " full_text_search_weighted_unaccent @@ websearch_to_tsquery('pali_unaccent', ?) ";
+                $key = $this->getWordEn($key[0]);
+                $param = [$key];
+                break;
+        };
+        return (['query'=>$queryWhere,'param'=>$param]);
+    }
+
+    private function getWordEn($strIn)
+    {
+        $out = str_replace(["ā","ī","ū","ṅ","ñ","ṭ","ḍ","ṇ","ḷ","ṃ"],
+                        ["a","i","u","n","n","t","d","n","l","m"], $strIn);
+        return ($out);
+    }
 }
 
-$param = getopt('p:');
+$port = Config['port'];
 
-if (!isset($param['p'])) {
-    echo 'parameter port is required. -p 9999  ';
+if (!isset($port)) {
+    echo 'parameter port is required. ';
     return;
 }
-$port = $param['p'];
 $server = new \Grpc\RpcServer();
 $server->addHttp2Port('0.0.0.0:' . $port);
 $server->handle(new Greeter());