Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ScrapBoの検索を追加 #9

Merged
merged 2 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading