Skip to content

Commit

Permalink
Merge pull request #9 from kmc-jp/add-scrapbox
Browse files Browse the repository at this point in the history
ScrapBoの検索を追加
  • Loading branch information
walnuts1018 authored Feb 8, 2024
2 parents 6548c56 + c0caa14 commit 4d9c3d7
Show file tree
Hide file tree
Showing 11 changed files with 569 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
HEINEKEN_ELASTIC_SEARCH_URL="http://host.docker.internal:9200/"
HEINEKEN_ELASTIC_SEARCH_PUKIWIKI_INDEX="pukiwiki"
HEINEKEN_ELASTIC_SEARCH_MAIL_INDEX="mail"
HEINEKEN_ELASTIC_SEARCH_SCRAPBOX_INDEX="scrapbox"

HEINEKEN_PUKIWIKI_BASE_URL="https://inside.kmc.gr.jp/wiki/"

HEINEKEN_MAIL_DEFAULT_CATEGORIES="info,"
HEINEKEN_MAIL_BASE_URL="https://inside.kmc.gr.jp/m2w/"

HEINEKEN_SCRAPBOX_BASE_URL="https://scrapbox.io/kmc/"
3 changes: 3 additions & 0 deletions app/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default function Navbar() {
<NavLink to="/search/mail" className="nav-item nav-link">
Mail
</NavLink>
<NavLink to="/search/scrapbox" className="nav-item nav-link">
ScrapBox
</NavLink>
<NavLink to="/help" className="nav-item nav-link">
Help
</NavLink>
Expand Down
36 changes: 36 additions & 0 deletions app/routes/help/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,42 @@ export default function Help() {
</div>
</div>`,
],
[false, `<h3>Scrapbox</h3>`],
`検索できるカラムは <code>title</code> (タイトル)、 <code>modified</code> (更新日時)、 <code>body</code> (内容) です。
デフォルト(クエリ上で、フィールドを何も指定しない状態)では <code>title</code> と <code>body</code> で検索されます。
フィールド名を指定することで、その他のカラムを用いて検索できます。`,
[
false,
`<div class="card">
<div class="card-body">
body:"環境構築" title:"kubernetes"
</div>
</div>`,
],
[
false,
`<div class="card">
<div class="card-body">
"Kubernetes" modified:[2021-01-01 TO 2023-04-04]
</div>
</div>`,
],
[
false,
`<div class="card">
<div class="card-body">
("Kubernetes" OR "k8s") AND "metallb"
</div>
</div>`,
],
[
false,
`<div class="card">
<div class="card-body">
"Kubernetes" -body:"k8s"
</div>
</div>`,
],
]}
/>
<HelpListContent
Expand Down
147 changes: 147 additions & 0 deletions app/routes/search.scrapbox/els-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Page, PageResult } from "./models";
import { toQueryString } from "~/utils";

export const SEARCH_SIZE = 35;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createPageFromResponse(json: any): Page {
return {
id: json._id,
title: json._source.title,
modified: json._source.modified,
body: json.highlight.body[0],
};
}

// From elasticsearch
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createPageResultFromResponse(json: any): PageResult {
return {
pages: json.hits.hits.map((item: unknown) => createPageFromResponse(item)),
totalCount: json.hits.total.value,
};
}

export function buildScrapBoxSearch(
order: string,
page: number,
useRawQuery: boolean,
query?: string,
) {
let queryString: string;
if (query == null || query == "") {
queryString = "*";
} else {
queryString = useRawQuery ? query : toQueryString(query);
}
return {
query: queryString,
size: SEARCH_SIZE,
from: (page - 1) * SEARCH_SIZE,
order: order,
};
}

interface ScrapBoxSearch {
query: string;
size: number;
from: number;
order: string;
}

export async function requestSearch({
query,
size,
from,
order,
}: ScrapBoxSearch) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryJson: any = {
query: {
function_score: {
query: {
query_string: {
// boost by title
fields: ["title^5", "body"],
query: query,
default_operator: "AND",
},
},
},
},
_source: ["modified", "title"],
size: size,
from: from,
};

// eslint-disable-next-line default-case
switch (order) {
case "m":
queryJson["sort"] = { modified: "desc" };
break;
case "ta":
queryJson["sort"] = { "title.keyword": "asc" };
break;
case "td":
queryJson["sort"] = { "title.keyword": "desc" };
break;
case "s":
queryJson["sort"] = ["_score", { modified: "desc" }];
queryJson["query"]["function_score"]["functions"] = [
{
// boost by date
exp: {
modified: {
// tekitou iikanji values~~
offset: "150d",
scale: "500d",
decay: 0.75,
},
},
},
{
script_score: {
// boost by title length (jakkan)
script: {
inline:
"_score / Math.sqrt(Math.log1p(doc['title.keyword'].value.length()))",
},
},
},
];
break;
}

queryJson["highlight"] = {
// html escape
encoder: "html",
fields: {
body: {
pre_tags: ["<mark>"],
post_tags: ["</mark>"],
fragment_size: 220,
no_match_size: 220,
number_of_fragments: 1,
},
},
};

const url = new URL(
"_search",
process.env.HEINEKEN_ELASTIC_SEARCH_URL! +
process.env.HEINEKEN_ELASTIC_SEARCH_SCRAPBOX_INDEX! +
"/",
);
url.searchParams.append("source", JSON.stringify(queryJson));
url.searchParams.append("source_content_type", "application/json");

const response = await fetch(url);
if (!response.ok) {
throw new Response(
`Invalid response from the backend elasticsearch server: ${response.statusText}`,
{ status: 500 },
);
}
const json = await response.json();
return createPageResultFromResponse(json);
}
12 changes: 12 additions & 0 deletions app/routes/search.scrapbox/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// scrapbox page
export interface Page {
id: string;
title: string;
modified: number;
body: string;
}

export interface PageResult {
pages: Page[];
totalCount: number;
}
15 changes: 15 additions & 0 deletions app/routes/search.scrapbox/page-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.PageListItem a h3 {
display: inline-block;
text-decoration: none;
max-width: 100%;
}

.PageListItem a h3:hover {
text-decoration: underline;
}

.PageListItem p.small {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
53 changes: 53 additions & 0 deletions app/routes/search.scrapbox/page-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Page, PageResult } from "./models";
import styles from "./page-list.css";
import { createScrapBoxUrl } from "./utils";
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LinksFunction } from "@remix-run/node";

export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];

interface PageListItemProps {
page: Page;
scrapboxBaseURL: string;
}

function PageListItem(props: PageListItemProps) {
// scrapboxのmodifiedはunix timestamp
const dateStr = new Date(props.page.modified * 1000).toLocaleString("ja-JP", {
timeZone: "Asia/Tokyo",
});
return (
<div className="row PageListItem mb-2">
<div className="col-md-10 offset-md-1">
<a href={createScrapBoxUrl(props.scrapboxBaseURL, props.page.title)}>
<h3>{props.page.title}</h3>
</a>
<p className="small mb-2">
<FontAwesomeIcon icon={faCalendar} />
&ensp;{dateStr}
</p>
<p dangerouslySetInnerHTML={{ __html: props.page.body }} />
</div>
</div>
);
}

interface PageListProps {
pageResult: PageResult;
scrapboxBaseURL: string;
}

export function PageList(props: PageListProps) {
return (
<div className="PageList">
{props.pageResult.pages.map((item) => (
<PageListItem
key={item.id}
page={item}
scrapboxBaseURL={props.scrapboxBaseURL}
/>
))}
</div>
);
}
Loading

0 comments on commit 4d9c3d7

Please sign in to comment.